Ir al contenido
getnextpdf.com

Ejecuta NextPDF en plataformas serverless

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 /tmp con 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 FontRegistry una 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.json y un composer.lock confirmados, con nextpdf/core como 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 serverless para 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 dependencia symfony/process existe 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 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 instead

No 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.

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:

handler.php (outline)
<?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 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/DocumentFactory acotado 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.
  • 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 /tmp acepta escrituras), y el sistema de archivos de un contenedor de Cloud Run / App Runner tiene escritura pero es efímero. Usa getPdfData() y devuelve/sube los bytes; deja en espera bajo /tmp o 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 /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. Empaqueta y registra los archivos. Consulta la página de fuentes enlazada arriba.
  • NEXTPDF_FONTS_PATH no hace nada para el motor desnudo. Es el valor predeterminado de configuración de nextpdf/laravel, no una variable que lea nextpdf/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. writeHtmlChrome necesita un Chromium headless y la vía de subproceso symfony/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 y writeHtml no 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-autoloader en 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: true con Content-Type: application/pdf, y configura la API para que trate application/pdf como 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.
  • No envíes dependencias de desarrollo. Instala con --no-dev para 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.

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.