Contenerizar una aplicación NextPDF
En resumen
Sección titulada «En resumen»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.jsony uncomposer.lockconfirmados, connextpdf/corecomo dependencia. - Tienes los archivos de fuente que pretendes incrustar y tienes licencia para incrustarlos.
- Puedes ejecutar
docker buildsobre 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.
Qué requiere realmente el motor
Sección titulada «Qué requiere realmente el motor»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ón | Por qué la necesita el motor |
|---|---|
ext-mbstring | Manejo de cadenas multibyte para texto y codificaciones |
ext-intl | Compatibilidad con Unicode, configuración regional e internacionalización |
ext-gd | Decodificación y procesamiento de imágenes rasterizadas |
ext-openssl | Criptografía para firma y hash seguro |
ext-zlib | Compresión de flujos (Flate) de objetos PDF |
ext-curl | Cliente 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»).
El Dockerfile de producción
Sección titulada «El Dockerfile de 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.*.localvar/cache/storage/node_modules/*.logExcluye 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 sí 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 /appCOPY 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/appRUN rm -rf /var/www/app/vendorCOPY --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/appUSER 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.
Empaquetar las fuentes en la imagen
Sección titulada «Empaquetar las fuentes en la imagen»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.
Ejecutar como un usuario sin privilegios
Sección titulada «Ejecutar como un usuario sin privilegios»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 para producción
Sección titulada «Opcache para producción»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=1opcache.enable_cli=0opcache.memory_consumption=192opcache.interned_strings_buffer=16opcache.max_accelerated_files=20000opcache.validate_timestamps=0opcache.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.
Verificar la imagen
Sección titulada «Verificar la imagen»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:
docker run --rm your-app:latest php vendor/bin/nextpdf doctorUn 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:
# Fail the pipeline if the engine's environment is not healthy.docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1Para 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.
Casos límite y trampas
Sección titulada «Casos límite y trampas»php:8.4-fpmo-apacheen 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 elCMD/punto de entrada. Para un worker de cola o un trabajo por lotes de CLI,-clies correcto.- Alpine (
php:8.4-alpine) necesita nombres de paquete diferentes. Las líneasapt-getde arriba son para la imagen predeterminada basada en Debian. En Alpine, instala las cabeceras*-devcomo 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 pasodocker-php-ext-install gd intl mbstring opcache,apk del .build-deps— pero primeroapk add --no-cachelas 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 desenlaceintl.so/gd.so/mbstring.so. Es la misma regla de conservar las bibliotecas de ejecución que el bloque de Debian impone conapt-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 excluyevendor/,.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: unCOPYde directorio es una fusión, no un reemplazo, de modo que unvendor/compilado en el host que llegara al contexto aterrizaría primero y elCOPY --from=vendor /app/vendor /var/www/app/vendorsolo sobrescribiría las rutas que contiene el árbol de dependencias limpio. En este Dockerfile elRUN rm -rf /var/www/app/vendorantes 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 salvaguardarm -rf, por lo que la exclusión del.dockerignorees la solución duradera.
Notas de seguridad
Sección titulada «Notas de seguridad»- No despliegues dependencias de desarrollo.
--no-devmantiene 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 appuserfinal 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.4a 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.
Conformidad
Sección titulada «Conformidad»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.
Véase también
Sección titulada «Véase también»- Aprovisionar fuentes para el motor nativo en producción: la nomenclatura de archivos de fuente, la API del registro y el patrón de calentar-y-bloquear en el que se apoya esta imagen.
- Transmitir un PDF generado grande como respuesta HTTP: el modelo de memoria para servir un documento construido desde un controlador de framework.
- Representar en el borde con Cloudflare: cuándo un contenedor en proceso no es el entorno de ejecución adecuado.
- Configuración del ionCube Loader: la preocupación de imagen separada para las compilaciones premium codificadas con ionCube.