Ir al contenido
getnextpdf.com

Contenerizar una aplicación NextPDF

Quieres una imagen Docker pequeña y reproducible que ejecute el motor del núcleo nativo, en proceso, de NextPDF — composer require nextpdf/core, generando PDF dentro de tu proceso de PHP. Esta página construye exactamente eso: una imagen php:8.4 con solo las extensiones que el motor realmente necesita, sin dependencias de desarrollo en la capa final, fuentes empaquetadas, un usuario de ejecución sin privilegios, opcache ajustado para producción y un paso de verificación que hace fallar la compilación si falta algo.

Esta página es únicamente para el motor nativo. El puente Chrome (writeHtmlChrome mediante nextpdf/artisan) y el servidor Connect son entornos de ejecución separados con sus propias imágenes más pesadas — una instalación de Chromium headless para el puente, un servicio de larga duración para Connect. No añadas un navegador ni un servidor a esta imagen; el motor nativo no necesita ninguno.

Antes de empezar, confirma que estas piezas están en su sitio:

  • Tu aplicación tiene un composer.json y un composer.lock confirmados, con nextpdf/core como dependencia.
  • Tienes los archivos de fuente que pretendes incrustar y tienes licencia para incrustarlos.
  • Puedes ejecutar docker build sobre el directorio de tu aplicación.

Esta es una guía práctica de operaciones. Aquí casi no hay PHP; el trabajo es el Dockerfile y unos pocos ajustes de entorno.

La imagen debe satisfacer las restricciones de plataforma reales del motor, nada más. Leyéndolas directamente del paquete, nextpdf/core requiere php: >=8.4 <9.0 y estas extensiones de PHP:

ExtensiónPor qué la necesita el motor
ext-mbstringManejo de cadenas multibyte para texto y codificaciones
ext-intlCompatibilidad con Unicode, configuración regional e internacionalización
ext-gdDecodificación y procesamiento de imágenes rasterizadas
ext-opensslCriptografía para firma y hash seguro
ext-zlibCompresión de flujos (Flate) de objetos PDF
ext-curlCliente HTTP para las llamadas salientes del motor

Asígnalas a la imagen oficial php:8.4. openssl, curl y zlib ya vienen compiladas en la imagen oficial de PHP, así que no las docker-php-ext-install. mbstring, gd e intl no vienen incluidas y deben instalarse, y cada una necesita primero sus cabeceras de desarrollo del sistema presentes: mbstring necesita además la dependencia de compilación libonig-dev (Oniguruma). No añadas extensiones del motor que el paquete no liste: cada docker-php-ext-install adicional es tiempo de compilación y superficie de ataque que no necesitas. La única extensión ajena al motor que esta imagen sí instala es opcache: es una extensión de rendimiento en tiempo de ejecución, no viene habilitada en la imagen oficial, y el ajuste de opcache de abajo depende de que esté presente (consulta «Opcache para producción»).

Esta es una compilación de dos etapas. La primera etapa instala las dependencias de Composer con los paquetes de desarrollo excluidos; la segunda etapa es la imagen de ejecución ligera que se despliega.

Primero añade un .dockerignore junto al Dockerfile. Su tarea principal es mantener el entorno del host — un vendor/ compilado en el host, archivos de secretos locales y cachés de compilación — completamente fuera del contexto de compilación, de modo que COPY . /var/www/app despliegue solo lo que pretendes: compilaciones más pequeñas, rápidas y seguras que no pueden filtrar secretos locales de .env ni cargar megabytes del vendor/ del host en la imagen.

Excluir vendor/ también importa porque un COPY de directorio es una fusión, no un reemplazo. El Dockerfile de abajo ejecuta RUN rm -rf /var/www/app/vendor antes del COPY --from=vendor ... /var/www/app/vendor, de modo que en esta imagen un vendor/ del host nunca puede sobrevivir bajo el árbol de dependencias limpio. Pero si alguna vez eliminas esa salvaguarda rm -rf, un vendor/ compilado en el host en el contexto aterrizaría primero y la copia de la etapa de vendor solo sobrescribiría las rutas que contiene el árbol limpio: cualquier archivo del host adicional (un paquete obsoleto o instalado en modo dev, una clase huérfana) sobreviviría entonces por debajo. Mantener vendor/ fuera del contexto cierra ese agujero independientemente del rm -rf.

# .dockerignore — keep the host environment out of the build context.
vendor/
.git/
.env
.env.local
.env.*.local
var/cache/
storage/
node_modules/
*.log

Excluye los archivos de secretos locales reales (.env, .env.local, .env.*.local), no un .env.* general: ese comodín también descarta plantillas no secretas como .env.example que quieres desplegar para que la imagen lleve una configuración de referencia documentada. Mantén en el contexto cualquier plantilla de entorno confirmada y no secreta; excluye solo los archivos que realmente contienen secretos locales.

# syntax=docker/dockerfile:1
# ---- Stage 1: dependencies (no dev) ---------------------------------------
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
# Install production dependencies only. --no-dev excludes phpunit, phpstan,
# infection, and the other require-dev tooling from the shipped image.
# --optimize-autoloader builds a class map for the *vendor* tree here; the
# application's own classes are not present in this stage yet, so they are
# optimized after the source copy in the runtime stage (see below).
RUN composer install \
--no-dev \
--no-interaction \
--no-progress \
--prefer-dist \
--optimize-autoloader \
--no-scripts
# ---- Stage 2: runtime ------------------------------------------------------
FROM php:8.4-cli AS runtime
# System headers for the gd, intl, and mbstring extensions that need compiling.
# The PHP image already provides openssl, curl, and zlib, so those are NOT
# listed; gd, intl, and mbstring are installed below. opcache has no system
# headers and is installed in the same step. mbstring is built against
# Oniguruma, so libonig-dev is in the *-dev set and its runtime lib (libonig5)
# is preserved by the same detection below.
#
# Build the *-dev headers (which pull in the runtime libs), compile the
# extensions, then mark only the runtime shared libraries the extensions
# actually link against so they survive the --auto-remove purge of the headers.
# Removing libicu / libpng / libjpeg / libfreetype / libonig here would unlink
# intl.so, gd.so, or mbstring.so at runtime ("undefined symbol" / "cannot open
# shared object file").
RUN set -eux; \
savedAptMark="$(apt-mark showmanual)"; \
apt-get update; \
apt-get install -y --no-install-recommends \
libicu-dev \
libpng-dev \
libjpeg62-turbo-dev \
libfreetype6-dev \
libonig-dev; \
docker-php-ext-configure gd --with-freetype --with-jpeg; \
docker-php-ext-install -j"$(nproc)" gd intl mbstring opcache; \
# Detect the runtime .so dependencies of the just-built extensions and
# mark them manual so --auto-remove keeps them while dropping the headers.
apt-mark auto '.*' > /dev/null; \
apt-mark manual $savedAptMark > /dev/null; \
find /usr/local/lib/php/extensions -type f -name '*.so' -exec \
sh -c 'ldd "$1" 2>/dev/null \
| awk "/=>/ { print \$3 }" \
| grep -E "^/" \
| xargs -r dpkg-query -S 2>/dev/null \
| cut -d: -f1 \
| sort -u \
| xargs -r apt-mark manual' _ {} \; ; \
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
rm -rf /var/lib/apt/lists/*
# Production opcache settings (see the opcache section below). The opcache
# extension is installed above (docker-php-ext-install opcache); this file only
# tunes it.
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
# A static Composer binary for the one optimized-autoloader rebuild below. It is
# copied into the build but the final stage runs no Composer at request time.
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/app
# Application code, then the vendor tree from the dependency stage. The .dockerignore
# should already keep a host vendor/ out of the context; the rm here is a second line
# of defense so a stale host-built vendor/ can never merge under the clean one (a
# directory COPY merges, it does not replace).
COPY . /var/www/app
RUN rm -rf /var/www/app/vendor
COPY --from=vendor /app/vendor /var/www/app/vendor
# Now that the application source is present, regenerate the optimized class map
# so the APP's own classes are in the optimized autoloader, not just the vendor
# packages. --no-dev keeps require-dev out; --no-scripts avoids running
# application hooks during the image build.
RUN composer dump-autoload \
--optimize \
--no-dev \
--no-interaction \
--no-scripts \
&& rm -f /usr/bin/composer
# The bundled fonts live at /var/www/app/resources/fonts. The native engine does
# NOT read any font-path environment variable — the entrypoint registers that
# directory in PHP (see "Bundle fonts into the image" below). There is no ENV
# line for fonts here.
# Run as a non-root user (see the non-root section below).
RUN useradd --system --no-create-home --uid 10001 appuser \
&& chown -R appuser:appuser /var/www/app
USER appuser
CMD ["php", "bin/generate.php"]

La etapa de dependencias se ejecuta con --no-scripts para que ningún hook de post-instalación de la aplicación se ejecute contra un árbol incompleto; ejecuta cualquier paso de compilación de la aplicación (compilación de assets, calentamiento de caché) en una etapa posterior, después de copiar el código.

Instalación multietapa de Composer (sin dependencias de desarrollo)

Sección titulada «Instalación multietapa de Composer (sin dependencias de desarrollo)»

La imagen desplegada no debe contener herramientas de desarrollo. El indicador --no-dev en composer install es la línea portante: omite todo lo que está bajo require-dev en nextpdf/core y en tu aplicación — el ejecutor de pruebas, el analizador estático y las herramientas de mutación —, nada de lo cual tiene lugar alguno en producción. Combínalo con --optimize-autoloader para que el autocargador sea un mapa de clases generado en lugar de un escaneo del sistema de archivos en cada solicitud.

Copia composer.json y composer.lock antes que el resto del código fuente para que Docker almacene en caché la capa de dependencias y solo vuelva a resolver cuando cambia el archivo de bloqueo. Como esa primera instalación se ejecuta solo contra el archivo de bloqueo — sin el código fuente de la aplicación —, --optimize-autoloader ahí construye el mapa de clases únicamente para el árbol de vendor; las propias clases de tu aplicación aún no están presentes. Por eso la etapa de ejecución ejecuta composer dump-autoload --optimize --no-dev --no-scripts una vez después de copiar el código: integra las clases de la app en el mismo mapa de clases optimizado. No ejecutes un composer dump-autoload aparte en un árbol de trabajo en el que también desarrollas (confirmaría un mapa de clases de producción en un árbol de dev); la reconstrucción pertenece a la imagen, después de la copia del código, como se muestra arriba.

El motor nativo resuelve las fuentes a partir de archivos de fuente que puede leer, no a partir de fuentes instaladas en el sistema operativo. Instalar paquetes fonts-* o ejecutar fc-cache no hace nada que la vía nativa pueda ver, así que esta imagen no instala fuentes del sistema. Empaqueta tus archivos .ttf / .otf bajo resources/fonts/; el COPY . /var/www/app de arriba ya los lleva a la imagen.

Llevar los archivos a la imagen es solo la mitad del trabajo. El motor nativo simple no lee ninguna variable de entorno de búsqueda de fuentes — NEXTPDF_FONTS_PATH es el valor predeterminado de la clave de configuración fonts_path del paquete nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) y solo la consume esa integración de framework, no nextpdf/core—. Un punto de entrada simple php bin/generate.php con solo esa variable establecida no registra ninguna fuente y representa el mismo tofu que esta imagen existe para evitar. El punto de entrada debe registrar el directorio empaquetado en PHP:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Register the directory the Dockerfile bundled the fonts into.
$registry = new FontRegistry('/var/www/app/resources/fonts');
// (equivalently, $registry->addFontDirectory('/var/www/app/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();

Esa es toda la preocupación de Docker respecto a las fuentes. Las reglas de nomenclatura de archivos, la API del registro, el patrón de calentar-y-bloquear y el manejo de sistemas de archivos de solo lectura residen todos en la página dedicada; no los dupliques aquí. Lee Aprovisionar fuentes para el motor nativo en producción para el patrón completo, y registra el mismo directorio que empaquetaste.

Las imágenes oficiales de PHP se ejecutan como root de forma predeterminada. Un generador de PDF no necesita root, así que crea un usuario sin privilegios y cambia a él. El Dockerfile de arriba añade un usuario de sistema appuser con un UID alto fijo (10001), le da la propiedad del árbol de la aplicación y termina con USER appuser para que todo proceso que el contenedor inicie no tenga privilegios.

Mantén la aplicación de solo lectura en tiempo de ejecución donde puedas. El motor lee sus archivos de fuente y escribe únicamente su salida y una caché opcional de fuentes analizadas, de modo que un contenedor con readOnlyRootFilesystem funciona siempre que la ruta de salida y cualquier directorio de caché sean montajes escribibles. Combina esto con capacidades de Linux descartadas y un indicador no-new-privileges en tu orquestador para una defensa en profundidad.

Opcache rinde para workers de PHP de larga duración — un grupo FPM o un proceso de Apache mod_php que sirve muchas solicitudes desde un proceso ya caliente. Esos procesos compilan tus clases una vez y luego nunca hacen stat de los archivos de origen en una vía caliente, que es exactamente lo que te compra opcache.validate_timestamps=0. Opcache no viene habilitado de fábrica en la imagen oficial php:8.4, así que el Dockerfile de arriba lo instala con docker-php-ext-install opcache (puedes equivalentemente docker-php-ext-enable opcache si la extensión ya está compilada). El archivo conf.d de abajo es ajuste, no el paso de habilitación: no hace nada hasta que la extensión se carga. Despliégalo como una inclusión conf.d (docker/opcache.ini, copiado en el Dockerfile):

opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

opcache.validate_timestamps=0 significa que la caché nunca vuelve a comprobar los archivos de origen — correcto para una imagen inmutable, ya que la única forma de que el código cambie es una imagen nueva. Ajusta memory_consumption y max_accelerated_files al número de clases de tu aplicación.

El CMD mostrado es un generador de CLI de una sola pasada, y opcache.enable_cli=0 es correcto para él. Un proceso php bin/generate.php de corta duración arranca, compila, representa una vez y sale, de modo que una caché de opcodes que no puede compartir con una solicitud siguiente no da ningún beneficio: deja la opcache de CLI desactivada y no pagues nada de su coste de memoria. Opcache solo se gana el sueldo donde el proceso se reutiliza: una SAPI FPM/Apache, o un worker de CLI genuinamente de larga duración (un consumidor de cola o un servidor estilo RoadRunner). Solo ese tipo de worker de CLI residente establecería opcache.enable_cli=1; para el generador de una sola pasada de aquí, mantenlo en 0.

Si ejecutas una configuración que usa precarga de opcache (un worker FPM de larga duración con un script opcache.preload), establece opcache.preload=/path/to/preload.php y añade opcache.preload_user=appuser para que la precarga se ejecute como el usuario sin privilegios. Sin un script opcache.preload real, opcache.preload_user no hace nada, por lo que no está en la configuración base de arriba; no lo añadas a menos que también establezcas opcache.preload.

Añade un paso de verificación para que una imagen mal construida falle de forma ruidosa en lugar de producir tofu o un error fatal en la primera solicitud. NextPDF incluye una CLI cuyo comando doctor inspecciona el entorno de PHP en ejecución e informa exactamente sobre las extensiones que le importan al motor — openssl, zlib, mbstring, gd, curl e intl. El paquete declara "bin": ["bin/nextpdf"], de modo que en una aplicación consumidora Composer instala el ejecutable en vendor/bin/nextpdf (no bin/nextpdf, que es la ruta dentro del propio paquete nextpdf/core). Ejecútalo dentro de la imagen construida:

Ventana de terminal
docker run --rm your-app:latest php vendor/bin/nextpdf doctor

Un resultado saludable confirma PHP 8.4 y que todas las extensiones requeridas están cargadas. Conecta la misma llamada en la compilación (o en un trabajo de humo de CI) para que una extensión ausente detenga la canalización:

Ventana de terminal
# Fail the pipeline if the engine's environment is not healthy.
docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1

Para una comprobación de extremo a extremo, representa una página a través de tu propio punto de entrada y haz aserciones sobre la salida, como describe la página de fuentes para una comprobación de humo de fuentes.

  • php:8.4-fpm o -apache en lugar de -cli. Usa la SAPI bajo la que tu app realmente se sirve. La lista de extensiones es idéntica; solo difieren la etiqueta base y el CMD/punto de entrada. Para un worker de cola o un trabajo por lotes de CLI, -cli es correcto.
  • Alpine (php:8.4-alpine) necesita nombres de paquete diferentes. Las líneas apt-get de arriba son para la imagen predeterminada basada en Debian. En Alpine, instala las cabeceras *-dev como un grupo virtual de compilación (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) y, tras el paso docker-php-ext-install gd intl mbstring opcache, apk del .build-deps — pero primero apk add --no-cache las bibliotecas de ejecución contra las que enlazan las extensiones (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma) para que eliminar el grupo de compilación no desenlace intl.so / gd.so / mbstring.so. Es la misma regla de conservar las bibliotecas de ejecución que el bloque de Debian impone con apt-mark.
  • No instales paquetes fonts-*. Son invisibles para el motor nativo. Empaqueta archivos de fuente en su lugar; consulta la página de fuentes enlazada arriba.
  • Premium e ionCube son una preocupación de imagen distinta. Las compilaciones de NextPDF Pro / Enterprise codificadas con ionCube necesitan el ionCube Loader instalado en la imagen y emparejado con la compilación exacta de PHP del contenedor (8.4, NTS frente a ZTS). Eso queda fuera del alcance de una imagen del núcleo; si despliegas premium, sigue la sección de Docker de Configuración del ionCube Loader.
  • Mantén un vendor/ del host fuera del contexto de compilación. El .dockerignore (que excluye vendor/, .git/ y las cachés locales) mantiene el árbol del host completamente fuera del contexto: eso es lo que hace la compilación pequeña, rápida y libre de secretos locales filtrados. También protege el caso de fusión de directorios: un COPY de directorio es una fusión, no un reemplazo, de modo que un vendor/ compilado en el host que llegara al contexto aterrizaría primero y el COPY --from=vendor /app/vendor /var/www/app/vendor solo sobrescribiría las rutas que contiene el árbol de dependencias limpio. En este Dockerfile el RUN rm -rf /var/www/app/vendor antes de la copia de vendor ya elimina cualquier directorio de ese tipo, de modo que ese residuo no puede ocurrir aquí; el riesgo de fusión solo regresa si eliminas esa salvaguarda rm -rf, por lo que la exclusión del .dockerignore es la solución duradera.
  • No despliegues dependencias de desarrollo. --no-dev mantiene las herramientas de prueba y análisis, y sus paquetes transitivos, fuera de la imagen de ejecución y de su superficie de ataque.
  • Ejecuta sin privilegios. El USER appuser final asegura que ningún proceso del contenedor se ejecute como root. Combínalo con un sistema de archivos raíz de solo lectura y capacidades descartadas en tu orquestador.
  • Fija la imagen base. Fija php:8.4 a un digest en producción para que una reconstrucción no pueda extraer silenciosamente una base modificada, y reconstruye con cadencia para incorporar parches de seguridad de forma deliberada.
  • Mantén las fuentes y las licencias fuera de las capas públicas. Empaqueta solo fuentes que tengas licencia para incrustar, y nunca incorpores un archivo de licencia premium en una imagen publicada públicamente; móntalo en tiempo de ejecución en su lugar.

Esta guía no hace ninguna afirmación normativa de estándares. Los hechos de plataforma se leen directamente del paquete nextpdf/core: la restricción php: >=8.4 <9.0 y las extensiones requeridas ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib y ext-curl. El comando de verificación es el manejador doctor real de la CLI nextpdf — declarado como "bin": ["bin/nextpdf"] en nextpdf/core y por tanto instalado en vendor/bin/nextpdf en una app consumidora —, que informa sobre el mismo conjunto de extensiones. El motor nativo registra las fuentes a través de NextPDF\Typography\FontRegistry (el argumento de directorio del constructor / addFontDirectory()) conectado mediante NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH es la clave de configuración fonts_path del paquete nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), no una variable que lea nextpdf/core. El comportamiento del registro está documentado en la página de fuentes enlazada en Véase también.