Ejecuta NextPDF en plataformas serverless
De un vistazo
Sección titulada «De un vistazo»El motor nativo en proceso del core de NextPDF es una carga de trabajo serverless
casi ideal. Es PHP puro que se ejecuta dentro de tu proceso:
composer require nextpdf/core, construir un documento, obtener los bytes. No hay
binario externo que invocar, ni navegador headless, ni demonio que mantener vivo,
ni socket hacia un servicio adjunto. Una función que construye un PDF arranca en
frío, ejecuta tu PHP, devuelve los bytes y termina. Eso encaja limpiamente con
AWS Lambda (a través del entorno de ejecución Bref), Google Cloud Run y AWS App
Runner.
Esta página cubre el despliegue de ese motor nativo en esos tres entornos de ejecución y el reducido conjunto de restricciones reales que imponen:
- el sistema de archivos del entorno de ejecución no es duradero: Lambda solo
garantiza un
/tmpcon permiso de escritura, mientras que los entornos de contenedor (Cloud Run, App Runner) tienen un sistema de archivos efímero y acotado al contenedor; en cualquier caso, las fuentes deben viajar dentro del paquete de despliegue o de la imagen y estar registradas en PHP (el motor no lee ninguna variable de entorno de ruta de fuentes); - los arranques en frío pagan la carga del autoloading y cualquier
calentamiento de fuentes, así que calienta el
FontRegistryuna vez por contenedor, no por invocación; - el tamaño del paquete, la memoria y el tiempo de espera deben dimensionarse según la construcción, no según una petición trivial.
Esta página es solo para el motor nativo. El puente con Chrome
(writeHtmlChrome mediante el paquete sugerido nextpdf/artisan) es una historia
distinta y más pesada: invoca un Chromium headless a través de symfony/process,
que un zip vanilla de Lambda o un contenedor ligero no contienen. Ejecutar
Chromium en Lambda implica una capa personalizada con el navegador y sus
bibliotecas compartidas, paquetes mucho más grandes y arranques en frío mucho más
largos: queda fuera del alcance aquí. El motor desnudo no necesita nada de eso.
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 dispones de licencia para incrustarlos.
- Tienes la cadena de herramientas de tu destino: la CLI de Bref y el framework
serverlesspara Lambda, o una construcción de contenedor para Cloud Run / App Runner.
Por qué el motor nativo encaja en serverless
Sección titulada «Por qué el motor nativo encaja en serverless»Leído directamente del paquete, nextpdf/core requiere php: >=8.4 <9.0 y un
pequeño conjunto de extensiones de PHP: ext-mbstring, ext-intl, ext-gd,
ext-openssl, ext-zlib y ext-curl. Las capas de PHP estándar de Bref incluyen
todas y cada una de ellas. Las imágenes de contenedor oficiales php:8.4
proporcionan openssl, curl y zlib de fábrica, pero mbstring, gd e intl
no vienen incluidas: requieren instalar dependencias del sistema y habilitar
las extensiones con docker-php-ext-install (consulta la
guía de despliegue con Docker).
En Bref no hay nada exótico que compilar; en la vía de contenedor habilitas esas
tres extensiones en la construcción de la imagen para el motor desnudo.
Lo que hace que el encaje sea limpio es lo que el motor no hace:
- Sin subproceso para la vía del core. Construir un documento y llamar a
getPdfData()es PHP en proceso de principio a fin. La dependenciasymfony/processexiste para el puente opcional con Chrome, no para el renderizado nativo: la generación nativa de PDF nunca lanza un proceso. - Sin estado persistente. Cada invocación construye un documento nuevo y devuelve los bytes. No tiene que sobrevivir nada entre peticiones salvo el contenedor caliente, que aprovechas para el calentamiento de fuentes (más abajo) pero del que nunca dependes para la corrección.
- Sin necesidad de un directorio de trabajo con permiso de escritura. El motor
construye el PDF en memoria y lo devuelve como una cadena; toca el disco solo si
tú llamas a
save(). En serverless no lo haces —devuelves los bytes—, así que la falta de un sistema de archivos duradero nunca afecta a la vía de construcción.
La única restricción dura: sin sistema de archivos duradero con escritura
Sección titulada «La única restricción dura: sin sistema de archivos duradero con escritura»El sistema de archivos de despliegue no es duradero, pero el modelo difiere según
el entorno de ejecución. AWS Lambda solo garantiza un /tmp con permiso de
escritura (512 MB de forma predeterminada, configurable hasta 10 GB); el resto
del sistema de archivos de la función es de solo lectura. Los entornos de
contenedor (Cloud Run, App Runner) tienen un sistema de archivos efímero, acotado
al contenedor y con permiso de escritura en lugar de un modelo limitado a
/tmp; pero todo lo que se escribe ahí se pierde cuando el contenedor se recicla,
así que es espacio de borrador, no almacenamiento. En todos los casos, prefiere
/tmp o un volumen configurado para el almacenamiento provisional, y nunca
dependas de las escrituras en la ruta de la imagen de la aplicación como
almacenamiento duradero. De ahí se derivan dos consecuencias.
Nunca llames a save() esperando salida duradera. NextPDF\Core\Document
expone tanto save(string $path): void como getPdfData(): string. En serverless
usas getPdfData() y devuelves o subes los bytes; no trates una escritura en el
directorio de la aplicación como almacenamiento persistente. Si tienes que dejar
un archivo en espera (por ejemplo, para subirlo por partes a almacenamiento de
objetos), escribe bajo /tmp (o un volumen configurado) y limpia, recordando que
en un contenedor caliente este espacio de borrador persiste entre invocaciones y
cuenta para su límite de tamaño.
use NextPDF\Core\Document;
// Right for serverless: get the bytes, return or upload them.$pdf = $document->getPdfData(); // string of PDF bytes, built in memory
// Avoid on serverless: save() writes to disk. On Lambda the application// directory is read-only; on Cloud Run / App Runner it is writable but// ephemeral (lost on container recycle). Neither is durable storage.// $document->save('/var/task/out.pdf'); // not durable — return the bytes insteadNo instales fuentes del sistema operativo en tiempo de ejecución, y no dependas
del descubrimiento automático de fuentes; empaqueta tus archivos de fuente para
producción. En Lambda, el sistema de archivos de solo lectura bloquea de plano
apt-get install fonts-*; en un entorno de contenedor, cualquier instalación en
tiempo de ejecución aterriza en un sistema de archivos efímero y se pierde en el
siguiente reciclaje. Y de todos modos no ayudaría, porque el motor nativo no lee
fuentes del SO/fontconfig: resuelve las fuentes solo a partir de los archivos que
registras. Así que, para producción, los archivos de fuente deben viajar dentro
del artefacto de despliegue. Si descargas deliberadamente archivos de fuente a
/tmp o a un volumen configurado, debes registrarlos explícitamente con el
registro de fuentes y aceptar el coste añadido de arranque en frío y de
fiabilidad: no es un patrón de producción recomendado.
Empaqueta y registra fuentes en el paquete o la imagen
Sección titulada «Empaqueta y registra fuentes en el paquete o la imagen»El motor nativo resuelve las fuentes a partir de archivos de fuente mediante
NextPDF\Typography\FontRegistry, no desde fontconfig ni desde fuentes
instaladas en el SO. En serverless esto es innegociable: no hay un sistema de
archivos persistente donde poner las fuentes después del despliegue, así que
viajan dentro del paquete (un zip o una capa de Lambda) o dentro de la imagen
(Cloud Run / App Runner).
Empaqueta tus archivos .ttf / .otf / .ttc bajo un directorio de tu proyecto
—resources/fonts/ es la convención— para que se incluyan en el artefacto.
Después, registra ese directorio en PHP. El motor no lee ninguna variable de
entorno de ruta 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 lo consume esa
integración de framework, no nextpdf/core. Una función desnuda debe construir el
registro con el directorio empaquetado:
use NextPDF\Typography\FontRegistry;use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;
// Register the directory the deployment artifact bundled the fonts into.// On Lambda/Bref the code root is /var/task; adjust for your runtime.$registry = new FontRegistry(__DIR__ . '/resources/fonts');// (equivalently, $registry->addFontDirectory(__DIR__ . '/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));$document = $factory->create();Esa es toda la preocupación serverless en lo que respecta a las fuentes. Las reglas de nombrado de archivos, la API completa del registro y el manejo del sistema de archivos no duradero viven en la página dedicada: no las dupliques aquí. Lee Aprovisiona fuentes para el motor nativo en producción para el patrón completo, y registra el mismo directorio que empaquetaste. La guía de despliegue con Docker cubre el empaquetado equivalente del lado de la imagen para el caso de Cloud Run / App Runner.
Arranques en frío: calienta el FontRegistry una vez por contenedor
Sección titulada «Arranques en frío: calienta el FontRegistry una vez por contenedor»Un arranque en frío paga el arranque de PHP, el autoloader optimizado de Composer y cualquier análisis de fuentes que dispare la primera construcción. No puedes evitar el arranque, pero sí puedes sacar el trabajo de fuentes de la vía caliente y reutilizarlo entre invocaciones calientes.
Construye el FontRegistry y el DocumentFactory una vez, fuera del handler,
para que vivan durante la vida del contenedor y se reutilicen en cada invocación
caliente. Opcionalmente, llama a warmup() con los archivos de fuente que sabes
que vas a usar, para que se analicen durante la inicialización en lugar de en el
primer renderizado, y luego bloquea el registro con lock() para que su estado
analizado quede congelado y ninguna mutación por invocación pueda competir:
use NextPDF\Typography\FontRegistry;use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;
// Container-scoped, built once at cold start (module scope, not per request).$fontsDir = __DIR__ . '/resources/fonts';$registry = new FontRegistry($fontsDir);
// Parse the fonts you will actually use now, so the first render does not.$registry->warmup([ $fontsDir . '/liberation/LiberationSans-Regular.ttf', $fontsDir . '/liberation/LiberationSans-Bold.ttf',]);
// Freeze the parsed state for the life of the warm container.$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// Each invocation: fresh document from the shared, warm factory.$handler = static function (array $event) use ($factory): string { $document = $factory->create(); $document->addPage(); $document->cell(0, 10, 'Hello from serverless', newLine: true);
return $document->getPdfData();};Llama a warmup() antes de lock(): el registro queda congelado una vez
bloqueado, así que un calentamiento posterior provoca un error de configuración.
Trata una fuente que no carga en el calentamiento como un error en tiempo de
despliegue, no como un detalle en tiempo de ejecución: valida que cada ruta de
fuente que pretendes calentar existe y se analiza realmente en el arranque, y haz
fallar el despliegue (o tu comprobación de salud) si alguna no lo hace, en lugar
de dejar que una ruta mal escrita aflore más tarde como glifos faltantes. Mantén
la lista de calentamiento limitada a las fuentes que necesita una invocación
típica; calentar una familia grande que rara vez usas solo alarga cada arranque en
frío.
Una función Bref en AWS Lambda
Sección titulada «Una función Bref en AWS Lambda»Bref proporciona el entorno de ejecución de PHP para Lambda
como una capa publicada y un plugin de serverless.yml. El entorno de ejecución
php-84 ya incluye las extensiones que necesita nextpdf/core, así que despliegas
tu código y tus fuentes y apuntas una función a un handler. Un serverless.yml
mínimo:
service: nextpdf-serverless
provider: name: aws region: us-east-1 runtime: provided.al2023
plugins: - ./vendor/bref/bref
functions: generate: handler: handler.php description: Generate a PDF with the native NextPDF engine runtime: php-84 memorySize: 1024 # size to the build; see "Sizing" below timeout: 30 # seconds; raise for large documents # The Lambda filesystem is read-only except /tmp. Fonts ship in the # package under resources/fonts and are registered in the handler.El handler construye el documento con la factoría caliente y acotada al contenedor
y devuelve los bytes. Para una API HTTP, devuélvelos codificados en base64 con el
tipo de contenido application/pdf para que API Gateway trate el cuerpo como
binario; para un disparador de invocación o de cola, sube los bytes a
almacenamiento de objetos y devuelve la clave:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// --- Cold-start: built once per container, reused across warm invocations. ---$fontsDir = __DIR__ . '/resources/fonts';$registry = new FontRegistry($fontsDir);$registry->warmup([$fontsDir . '/liberation/LiberationSans-Regular.ttf']);$registry->lock();$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// --- Per-invocation handler. ---return static function (array $event) use ($factory): array { $document = $factory->create(); $document->addPage(); $document->cell(0, 10, 'Invoice', newLine: true);
// getPdfData() materializes the whole PDF in memory and returns it. $bytes = $document->getPdfData();
return [ 'statusCode' => 200, 'isBase64Encoded' => true, 'headers' => ['Content-Type' => 'application/pdf'], 'body' => base64_encode($bytes), ];};Verifica que el paquete contiene un entorno saludable antes de dirigir tráfico
hacia él. nextpdf/core incluye una CLI instalada en vendor/bin/nextpdf cuyo
comando doctor informa exactamente sobre las extensiones que necesita el motor.
Ejecútalo una vez contra la misma imagen o capa del entorno de ejecución para
confirmar que PHP 8.4 y todas las extensiones requeridas están presentes.
Cloud Run y App Runner
Sección titulada «Cloud Run y App Runner»Cloud Run y App Runner ejecutan un contenedor en lugar de una función
comprimida, así que la construcción es la imagen Docker de
Empaqueta una aplicación NextPDF en un contenedor,
no un paquete de Bref. Las restricciones del motor nativo son idénticas:
empaqueta las fuentes en la imagen, registra el directorio empaquetado en PHP,
ejecuta sin privilegios y trata el sistema de archivos como no duradero. A
diferencia del modelo limitado a /tmp de Lambda, un contenedor de Cloud Run /
App Runner tiene un sistema de archivos efímero, acotado al contenedor y con
permiso de escritura; pero se reinicia en cada reciclaje, así que usa /tmp (un
tmpfs en Cloud Run) o un volumen configurado para borrador y nunca dependas de las
escrituras en la ruta de la imagen de la aplicación como almacenamiento duradero.
Las diferencias respecto a Lambda son operativas, no estructurales:
- El contenedor puede mantenerse caliente entre peticiones bajo un ajuste de
concurrencia, así que el calentamiento del
FontRegistry/DocumentFactoryacotado al contenedor de más arriba rinde a lo largo de muchas peticiones, no solo de la siguiente invocación. - Sirves sobre HTTP (un SAPI de FPM o del servidor PHP integrado) en lugar de un evento de invocación, así que devuelves los bytes a través de la respuesta de tu framework. Para un documento grande, devuélvelos como respuesta por streaming: consulta Entrega por streaming un PDF generado grande como respuesta HTTP.
- El tiempo de espera de la petición y la memoria se establecen en el servicio (tiempo de espera / memoria del servicio de Cloud Run; configuración de instancia de App Runner) en lugar de por función.
Todo lo demás —el conjunto de extensiones, el registro de fuentes, la llamada de
salida getPdfData()— es el mismo código que el handler de Lambda.
Dimensionado: paquete, memoria y tiempo de espera
Sección titulada «Dimensionado: paquete, memoria y tiempo de espera»- Tamaño del paquete y la imagen. El artefacto lleva
vendor/(solo producción: instala con--no-dev) y tus fuentes empaquetadas. Las fuentes dominan: una familia CJK completa son decenas de megabytes. Envía solo las fuentes que realmente renderizas para mantener el paquete de Lambda por debajo de sus límites y la imagen pequeña, lo que además acorta los arranques en frío. La familia Liberation incluida (resources/fonts/liberation/) es pequeña y cubre la sustitución de Helvetica con compatibilidad métrica. - Memoria.
getPdfData()construye el documento entero en memoria y lo devuelve como una sola cadena, así que la memoria pico es aproximadamente el tamaño de un PDF terminado más el conjunto de trabajo de la construcción. Dimensiona la memoria de la función/contenedor según el documento más grande que generes, no según un promedio. En Lambda, la memoria también escala la CPU, así que más memoria a menudo significa una construcción más rápida y una ejecución más barata pese a la tarifa por milisegundo más alta: mide ambas. Un documento de pocas páginas va cómodo con 512–1024 MB; los documentos con muchas imágenes o muchas páginas necesitan más. - Tiempo de espera. La construcción, no la transferencia, domina el presupuesto de la petición. Establece el tiempo de espera de la función por encima del peor tiempo de construcción con margen. Si un documento es lo bastante grande como para arriesgar un tiempo de espera agotado, mueve la generación a un disparador asíncrono (un Lambda respaldado por cola o un job de Cloud Run) que escriba el resultado en almacenamiento de objetos en lugar de bloquear una petición síncrona.
- Tamaño de
/tmp. Si dejas algo en espera bajo/tmp, ten en cuenta su límite de tamaño y recuerda que persiste entre invocaciones calientes: limpia, o un contenedor de larga vida lo llenará lentamente.
Casos límite y trampas
Sección titulada «Casos límite y trampas»- Sin
save()duradero al directorio de la app. El sistema de archivos de despliegue no es duradero: el directorio de la app de Lambda es de solo lectura (solo/tmpacepta escrituras), y el sistema de archivos de un contenedor de Cloud Run / App Runner tiene escritura pero es efímero. UsagetPdfData()y devuelve/sube los bytes; deja en espera bajo/tmpo un volumen configurado si hace falta. - No dependas del descubrimiento automático de fuentes. No instales fuentes
del SO en tiempo de ejecución, y no dependas del descubrimiento automático de
fuentes; empaqueta tus archivos de fuente para producción. El motor nativo no lee
fuentes del SO/fontconfig: resuelve solo los archivos que registras. Si descargas
deliberadamente archivos de fuente a
/tmpo a un volumen configurado, debes registrarlos explícitamente con el registro de fuentes y aceptar el coste añadido de arranque en frío y de fiabilidad. Empaqueta y registra los archivos. Consulta la página de fuentes enlazada arriba. NEXTPDF_FONTS_PATHno hace nada para el motor desnudo. Es el valor predeterminado de configuración denextpdf/laravel, no una variable que leanextpdf/core. Un handler de Bref desnudo que solo establece esa variable no registra ninguna fuente y renderiza tofu.- El puente con Chrome no encaja en una función vanilla.
writeHtmlChromenecesita un Chromium headless y la vía de subprocesosymfony/process. Poner Chromium en Lambda requiere una capa personalizada con el navegador y sus bibliotecas, paquetes mucho más grandes y arranques en frío largos. El motor nativo ywriteHtmlno necesitan nada de eso: prefiérelos en serverless. - El coste del arranque en frío es autoload más análisis de fuentes. Usa
--optimize-autoloaderen la instalación de producción y calienta el registro una vez por contenedor. No calientes fuentes que rara vez uses. - API Gateway necesita manejo binario. Devuelve
isBase64Encoded: trueconContent-Type: application/pdf, y configura la API para que trateapplication/pdfcomo tipo de medio binario, o el cliente recibirá bytes corruptos. - Premium e ionCube son una preocupación de artefacto más pesada. Las compilaciones de NextPDF Pro / Enterprise codificadas con ionCube necesitan el ionCube Loader emparejado con la compilación exacta de PHP del entorno de ejecución, que una capa de Bref de serie no incluye. Eso queda fuera del alcance de un despliegue serverless de core.
Notas de seguridad
Sección titulada «Notas de seguridad»- No envíes dependencias de desarrollo. Instala con
--no-devpara que las herramientas de prueba y de análisis nunca entren en el paquete o la imagen de la función. - Valida la entrada antes de construir. Una construcción de PDF dirigida por la entrada de la petición es un vector de agotamiento de memoria; rechaza las entradas fuera de rango o de tamaño excesivo en la frontera antes de que se ejecute cualquier trabajo de construcción, y acota la concurrencia para que el tráfico alto no multiplique la memoria pico hasta un fallo por falta de memoria.
- Mantén las fuentes y las licencias fuera de los artefactos públicos. Empaqueta solo fuentes para las que tengas licencia de incrustación, y nunca cocines un archivo de licencia premium dentro de una imagen o capa publicada de forma pública: en su lugar, suminístralo en tiempo de ejecución mediante un valor de entorno o un gestor de secretos.
- Mínimo privilegio. Da a la función/servicio solo los permisos IAM que necesita (por ejemplo, acceso de escritura al único bucket de salida), y ejecuta el contenedor sin privilegios como muestra la guía de Docker.
Conformidad
Sección titulada «Conformidad»Esta guía no hace ninguna afirmación normativa de estándares. Los hechos de la
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. La capa estándar del entorno de
ejecución de PHP-8.4 de Bref incluye las seis; la imagen oficial php:8.4
proporciona openssl, curl y zlib, pero mbstring, gd e intl deben
instalarse y habilitarse en la construcción de la imagen con
docker-php-ext-install (consulta la página de Docker). La llamada de salida es la
superficie real del core
NextPDF\Core\Document::getPdfData(): string (su equivalente en disco es
save(string $path): void). Las fuentes se registran mediante
NextPDF\Typography\FontRegistry —su argumento de directorio en el constructor /
addFontDirectory(), con warmup(array $fontFiles) y lock() para el patrón de
arranque en frío—, conectadas mediante NextPDF\Core\DocumentFactory::create().
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 comando doctor de la CLI nextpdf se declara
como "bin": ["bin/nextpdf"] en el paquete y se instala en vendor/bin/nextpdf en
una app consumidora. Los nombres del entorno de ejecución de Bref y los
comportamientos de AWS Lambda / Cloud Run / App Runner son funciones documentadas
de esos proveedores.
Véase también
Sección titulada «Véase también»- Empaqueta una aplicación NextPDF en un contenedor: la imagen de producción usada para los destinos Cloud Run / App Runner.
- Aprovisiona fuentes para el motor nativo en producción: el nombrado de archivos de fuente, la API del registro y el patrón de calentamiento y bloqueo en el que se apoya esta página.
- Entrega por streaming un PDF generado grande como respuesta HTTP: el modelo de memoria para devolver un documento construido sobre HTTP en Cloud Run / App Runner.
- Renderiza en el edge con Cloudflare: cuándo una función en proceso no es el entorno de ejecución adecuado.