Pular para o conteúdo
getnextpdf.com

Execute o NextPDF em plataformas serverless

O mecanismo core nativo e in-process do NextPDF é uma carga de trabalho serverless quase ideal. Ele é PHP puro executando dentro do seu processocomposer require nextpdf/core, construa um documento, obtenha os bytes. Não há binário externo para iniciar, nenhum navegador headless, nenhum daemon para manter vivo e nenhum socket para um serviço sidecar. Uma função que constrói um PDF inicia a frio, executa seu PHP, retorna os bytes e termina. Isso se mapeia de forma limpa para o AWS Lambda (por meio do runtime Bref), o Google Cloud Run e o AWS App Runner.

Esta página aborda a implantação desse mecanismo nativo nesses três runtimes e o pequeno conjunto de restrições reais que eles impõem:

  • o sistema de arquivos do runtime não é durável: o Lambda garante apenas um /tmp gravável, enquanto os runtimes de contêiner (Cloud Run, App Runner) têm um sistema de arquivos efêmero, com escopo de contêiner — de qualquer forma, as fontes devem viajar dentro do pacote de implantação ou imagem e ser registradas no PHP (o mecanismo não lê nenhuma variável de ambiente de caminho de fontes);
  • os cold starts pagam pelo autoloading e por qualquer aquecimento de fontes, então aqueça o FontRegistry uma vez por contêiner, não por invocação;
  • o tamanho do pacote, a memória e o timeout devem ser dimensionados para o build, não para uma requisição trivial.

Esta página é apenas para o mecanismo nativo. A ponte Chrome (writeHtmlChrome por meio do pacote sugerido nextpdf/artisan) é uma história diferente e mais pesada: ela aciona um Chromium headless por meio do symfony/process, que um zip Lambda padrão ou um contêiner enxuto não contém. Executar o Chromium no Lambda significa uma layer personalizada com o navegador e suas bibliotecas compartilhadas, pacotes muito maiores e cold starts muito mais longos — fora do escopo aqui. O mecanismo puro não precisa de nada disso.

Antes de começar, confirme se estas peças estão no lugar:

  • Sua aplicação tem um composer.json e um composer.lock commitados, com nextpdf/core como dependência.
  • Você tem os arquivos de fonte que pretende incorporar, e você está licenciado para incorporá-los.
  • Você tem o toolchain do seu alvo — a CLI do Bref e o framework serverless para Lambda, ou um build de contêiner para Cloud Run / App Runner.

Por que o mecanismo nativo se encaixa em serverless

Seção intitulada “Por que o mecanismo nativo se encaixa em serverless”

Lido diretamente do pacote, o nextpdf/core requer php: >=8.4 <9.0 e um pequeno conjunto de extensões PHP — ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib e ext-curl. As layers PHP padrão do Bref empacotam todas elas. As imagens de contêiner oficiais php:8.4 fornecem openssl, curl e zlib de fábrica, mas mbstring, gd e intl não são empacotadas — elas exigem instalar dependências do sistema e habilitar as extensões com docker-php-ext-install (consulte o guia de implantação com Docker). No Bref não há nada exótico para compilar; no caminho de contêiner você habilita essas três extensões no build da imagem para o mecanismo puro.

O que torna o encaixe limpo é o que o mecanismo não faz:

  • Nenhum subprocesso para o caminho core. Construir um documento e chamar getPdfData() é PHP in-process de ponta a ponta. A dependência symfony/process existe para a ponte Chrome opcional, não para a renderização nativa — a geração nativa de PDF nunca inicia um processo.
  • Nenhum estado persistente. Cada invocação constrói um documento novo e retorna bytes. Nada precisa sobreviver entre requisições, exceto o contêiner aquecido, que você explora para o aquecimento de fontes (abaixo) mas nunca usa como base para correção.
  • Nenhum diretório de trabalho gravável necessário. O mecanismo constrói o PDF em memória e o retorna como uma string; ele toca o disco apenas se você chamar save(). Em serverless você não chama — você retorna os bytes — então a falta de um sistema de arquivos durável nunca afeta o caminho de build.

A única restrição rígida: nenhum sistema de arquivos gravável durável

Seção intitulada “A única restrição rígida: nenhum sistema de arquivos gravável durável”

O sistema de arquivos de implantação não é durável, mas o modelo difere por runtime. O AWS Lambda garante apenas um /tmp gravável (512 MB por padrão, configurável até 10 GB); o resto do sistema de arquivos da função é somente leitura. Os runtimes de contêiner (Cloud Run, App Runner) têm um sistema de arquivos gravável efêmero, com escopo de contêiner em vez de um modelo apenas /tmp — mas qualquer coisa escrita ali é perdida quando o contêiner é reciclado, então é espaço de rascunho, não armazenamento. Em todos os casos, prefira /tmp ou um volume configurado para staging, e nunca use escritas no caminho da imagem da aplicação como armazenamento durável. Duas consequências decorrem disso.

Nunca chame save() esperando uma saída durável. NextPDF\Core\Document expõe tanto save(string $path): void quanto getPdfData(): string. Em serverless você usa getPdfData() e retorna ou faz upload dos bytes — não trate uma escrita no diretório da aplicação como armazenamento persistente. Se você precisa fazer staging de um arquivo (por exemplo, para fazer upload multipart para armazenamento de objetos), escreva sob /tmp (ou um volume configurado) e faça a limpeza, lembrando que em um contêiner aquecido esse espaço de rascunho persiste entre invocações e conta para o limite de tamanho dele.

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

Não instale fontes do SO em tempo de execução, e não confie na descoberta automática de fontes; empacote seus arquivos de fonte para produção. No Lambda o sistema de arquivos somente leitura bloqueia apt-get install fonts-* por completo; em um runtime de contêiner qualquer instalação em tempo de execução cai em um sistema de arquivos efêmero e é perdida na próxima reciclagem. E isso não ajudaria de qualquer forma, porque o mecanismo nativo não lê fontes do SO/fontconfig — ele resolve fontes apenas a partir de arquivos que você registra. Então, para produção, os arquivos de fonte devem ir dentro do artefato de implantação. Se você deliberadamente buscar arquivos de fonte para /tmp ou um volume configurado, você deve registrá-los explicitamente no registro de fontes e aceitar o custo adicional de cold start e de confiabilidade — não é um padrão de produção recomendado.

O mecanismo nativo resolve fontes a partir de arquivos de fonte por meio do NextPDF\Typography\FontRegistry, não a partir do fontconfig ou de fontes instaladas pelo SO. Em serverless isso é inegociável: não há sistema de arquivos persistente onde colocar fontes após a implantação, então elas vão dentro do pacote (um zip ou layer Lambda) ou dentro da imagem (Cloud Run / App Runner).

Empacote seus arquivos .ttf / .otf / .ttc sob um diretório no seu projeto — resources/fonts/ é a convenção — para que sejam incluídos no artefato. Em seguida, registre esse diretório no PHP. O mecanismo não lê nenhuma variável de ambiente de caminho de fontes: NEXTPDF_FONTS_PATH é o valor padrão da chave de configuração fonts_path do pacote nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) e é consumido apenas por essa integração de framework, não pelo nextpdf/core. Uma função pura deve construir o registro com o diretório empacotado:

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();

Essa é toda a preocupação serverless com fontes. As regras de nomeação de arquivos, a API completa do registro e o tratamento de sistema de arquivos não durável vivem na página dedicada — não os duplique aqui. Leia Provisione fontes para o mecanismo nativo em produção para o padrão completo, e registre o mesmo diretório que você empacotou. O guia de implantação com Docker cobre o empacotamento equivalente do lado da imagem para o caso Cloud Run / App Runner.

Cold starts: aqueça o FontRegistry uma vez por contêiner

Seção intitulada “Cold starts: aqueça o FontRegistry uma vez por contêiner”

Um cold start paga pelo bootstrap do PHP, pelo autoloader otimizado do Composer e por qualquer análise (parsing) de fontes que o primeiro build dispare. Você não pode evitar o bootstrap, mas pode tirar o trabalho de fontes do caminho quente e reutilizá-lo entre invocações aquecidas.

Construa o FontRegistry e o DocumentFactory uma vez, fora do handler, para que vivam pela vida do contêiner e sejam reutilizados em toda invocação aquecida. Opcionalmente chame warmup() com os arquivos de fonte que você sabe que vai usar, para que sejam analisados durante a inicialização em vez de na primeira renderização, e então use lock() no registro para que seu estado analisado seja congelado e nenhuma mutação por invocação possa entrar em condição de corrida:

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();
};

Chame warmup() antes de lock() — o registro fica congelado uma vez travado, então um warmup depois disso levanta um erro de configuração. Trate uma fonte que falha ao carregar no warmup como um erro em tempo de implantação, não um detalhe de tempo de execução: valide que todo caminho de fonte que você pretende aquecer realmente existe e é analisável na inicialização, e reprove a implantação (ou seu health check) se algum não for, em vez de deixar um caminho com erro de digitação aparecer mais tarde como glifos ausentes. Mantenha a lista de warmup nas fontes que uma invocação típica precisa; aquecer uma família grande que você raramente usa apenas alonga cada cold start.

O Bref fornece o runtime PHP para o Lambda como uma layer publicada e um plugin serverless.yml. O runtime php-84 já vem com as extensões que o nextpdf/core precisa, então você implanta seu código e fontes e aponta uma função para um handler. Um 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.

O handler constrói o documento com a factory aquecida, com escopo de contêiner, e retorna os bytes. Para uma API HTTP, retorne-os codificados em base64 com o content type application/pdf para que o API Gateway trate o corpo como binário; para um gatilho de invoke ou fila, faça upload dos bytes para o armazenamento de objetos e retorne a chave:

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),
];
};

Verifique se o pacote contém um ambiente saudável antes de direcionar tráfego para ele. O nextpdf/core traz uma CLI instalada em vendor/bin/nextpdf cujo comando doctor relata exatamente as extensões que o mecanismo precisa. Execute-o uma vez contra a mesma imagem de runtime ou layer para confirmar que o PHP 8.4 e toda extensão necessária estão presentes.

O Cloud Run e o App Runner executam um contêiner em vez de uma função zipada, então o build é a imagem Docker de Containerize a NextPDF application, não um pacote Bref. As restrições do mecanismo nativo são idênticas: empacote as fontes na imagem, registre o diretório empacotado no PHP, execute sem privilégios e trate o sistema de arquivos como não durável. Diferentemente do modelo apenas /tmp do Lambda, um contêiner Cloud Run / App Runner tem um sistema de arquivos gravável efêmero, com escopo de contêiner — mas ele é reiniciado a cada reciclagem, então use /tmp (um tmpfs no Cloud Run) ou um volume configurado para rascunho e nunca use escritas no caminho da imagem da aplicação como armazenamento durável.

As diferenças em relação ao Lambda são operacionais, não estruturais:

  • O contêiner pode permanecer aquecido entre requisições sob uma configuração de concorrência, então o warmup do FontRegistry/DocumentFactory com escopo de contêiner acima compensa em muitas requisições, não apenas na próxima invocação.
  • Você serve por HTTP (uma SAPI FPM ou de servidor PHP embutido) em vez de um evento de invoke, então você retorna os bytes pela resposta do seu framework. Para um documento grande, retorne-os como uma resposta em streaming — consulte Faça streaming de um PDF grande gerado como resposta HTTP.
  • O timeout e a memória da requisição são definidos no serviço (timeout / memória do serviço Cloud Run; configuração de instância do App Runner) em vez de por função.

Todo o resto — o conjunto de extensões, o registro de fontes, a chamada de saída getPdfData() — é o mesmo código do handler do Lambda.

  • Tamanho do pacote e da imagem. O artefato carrega vendor/ (apenas produção — instale com --no-dev) e suas fontes empacotadas. As fontes dominam: uma família CJK completa tem dezenas de megabytes. Envie apenas as fontes que você de fato renderiza para manter o pacote Lambda abaixo dos seus limites e a imagem pequena, o que também encurta os cold starts. A família Liberation empacotada (resources/fonts/liberation/) é pequena e cobre a substituição de Helvetica metric-compatible.
  • Memória. getPdfData() constrói o documento inteiro em memória e o retorna como uma única string, então o pico de memória é aproximadamente o tamanho de um PDF finalizado mais o working set do build. Dimensione a memória da função/contêiner para o maior documento que você gera, não para uma média. No Lambda, a memória também escala a CPU, então mais memória muitas vezes significa um build mais rápido e uma execução mais barata apesar da taxa por milissegundo mais alta — meça os dois. Um documento de poucas páginas é confortável em 512–1024 MB; documentos pesados em imagens ou com muitas páginas precisam de mais.
  • Timeout. O build, não a transferência, domina o orçamento da requisição. Defina o timeout da função acima do tempo de build do pior caso com margem. Se um documento for grande o suficiente para arriscar um timeout, mova a geração para um gatilho assíncrono (um Lambda baseado em fila ou um job do Cloud Run) que escreve o resultado no armazenamento de objetos em vez de bloquear uma requisição síncrona.
  • Tamanho do /tmp. Se você fizer staging de qualquer coisa sob /tmp, considere seu limite de tamanho e lembre que ele persiste entre invocações aquecidas — faça a limpeza, ou um contêiner de longa vida o preenche lentamente.
  • Sem save() durável para o diretório da aplicação. O sistema de arquivos de implantação não é durável — o diretório da aplicação do Lambda é somente leitura (apenas /tmp aceita escritas), e um sistema de arquivos de contêiner Cloud Run / App Runner é gravável mas efêmero. Use getPdfData() e retorne/faça upload dos bytes; faça staging sob /tmp ou um volume configurado se for necessário.
  • Não confie na descoberta automática de fontes. Não instale fontes do SO em tempo de execução, e não confie na descoberta automática de fontes; empacote seus arquivos de fonte para produção. O mecanismo nativo não lê fontes do SO/fontconfig — ele resolve apenas arquivos que você registra. Se você deliberadamente buscar arquivos de fonte para /tmp ou um volume configurado, você deve registrá-los explicitamente no registro de fontes e aceitar o custo adicional de cold start e de confiabilidade. Empacote e registre os arquivos. Consulte a página de fontes linkada acima.
  • NEXTPDF_FONTS_PATH não faz nada para o mecanismo puro. Ele é o padrão de configuração do nextpdf/laravel, não uma variável que o nextpdf/core lê. Um handler Bref puro que define apenas essa variável não registra nenhuma fonte e renderiza tofu.
  • A ponte Chrome não se encaixa em uma função padrão. writeHtmlChrome precisa de um Chromium headless e do caminho de subprocesso symfony/process. Colocar o Chromium no Lambda exige uma layer personalizada com o navegador e suas bibliotecas, pacotes muito maiores e cold starts longos. O mecanismo nativo e o writeHtml não precisam de nada disso — prefira-os em serverless.
  • O custo de cold start é autoload mais parse de fontes. Use --optimize-autoloader na instalação de produção e aqueça o registro uma vez por contêiner. Não aqueça fontes que você raramente usa.
  • O API Gateway precisa de tratamento binário. Retorne isBase64Encoded: true com Content-Type: application/pdf, e configure a API para tratar application/pdf como um tipo de mídia binário, ou o cliente recebe bytes corrompidos.
  • Premium e ionCube são uma preocupação de artefato mais pesada. Builds NextPDF Pro / Enterprise codificados com ionCube precisam do ionCube Loader correspondente ao build exato de PHP no runtime, que uma layer Bref padrão não inclui. Isso está fora do escopo de uma implantação serverless core.
  • Não envie dependências de desenvolvimento. Instale com --no-dev para que o ferramental de teste e análise nunca entre no pacote ou imagem da função.
  • Valide a entrada antes de construir. Um build de PDF guiado por entrada de requisição é um vetor de exaustão de memória; rejeite entradas fora do intervalo ou grandes demais na fronteira antes que qualquer trabalho de build seja executado, e limite a concorrência para que tráfego alto não multiplique o pico de memória em uma falha de out-of-memory.
  • Mantenha fontes e licenças fora de artefatos públicos. Empacote apenas fontes que você está licenciado para incorporar, e nunca embuta um arquivo de licença premium em uma imagem ou layer enviada publicamente — forneça-o em tempo de execução por meio de um valor de ambiente ou gerenciador de segredos.
  • Privilégio mínimo. Dê à função/serviço apenas as permissões IAM que ela precisa (por exemplo, acesso de escrita ao único bucket de saída), e execute o contêiner sem privilégios, como o guia do Docker mostra.

Este guia não faz nenhuma declaração normativa de padrões. Os fatos da plataforma são lidos diretamente do pacote nextpdf/core: a restrição php: >=8.4 <9.0 e as extensões necessárias ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib e ext-curl. A layer de runtime PHP-8.4 padrão do Bref empacota todas as seis; a imagem oficial php:8.4 fornece openssl, curl e zlib, mas mbstring, gd e intl devem ser instaladas e habilitadas no build da imagem com docker-php-ext-install (consulte a página do Docker). A chamada de saída é a superfície core real NextPDF\Core\Document::getPdfData(): string (sua irmã de disco é save(string $path): void). As fontes são registradas por meio do NextPDF\Typography\FontRegistry — seu argumento de construtor de diretório / addFontDirectory(), com warmup(array $fontFiles) e lock() para o padrão de cold start — conectado via NextPDF\Core\DocumentFactory::create(). NEXTPDF_FONTS_PATH é a chave de configuração fonts_path do pacote nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), não uma variável que o nextpdf/core lê. O comando doctor da CLI nextpdf é declarado como "bin": ["bin/nextpdf"] no pacote e instalado em vendor/bin/nextpdf em uma aplicação consumidora. Os nomes de runtime do Bref e os comportamentos de AWS Lambda / Cloud Run / App Runner são recursos documentados desses fornecedores.