Coloque uma aplicação NextPDF em contêiner
Visão geral
Seção intitulada “Visão geral”Você quer uma imagem Docker pequena e reproduzível que rode o engine NextPDF core
nativo, no mesmo processo — composer require nextpdf/core, gerando PDFs dentro
do seu processo PHP. Esta página constrói exatamente isso: uma imagem php:8.4 com
apenas as extensões que o engine realmente precisa, sem dependências de
desenvolvimento na camada final, fontes empacotadas, um usuário de runtime não-root,
opcache ajustado para produção e uma etapa de verificação que reprova o build se
algo estiver faltando.
Esta página é apenas para o engine nativo. A ponte do Chrome
(writeHtmlChrome via nextpdf/artisan) e o servidor Connect são runtimes
separados com suas próprias imagens mais pesadas — uma instalação de Chromium
headless para a ponte, um serviço de longa duração para o Connect. Não adicione um
navegador ou um servidor a esta imagem; o engine nativo não precisa de nenhum dos
dois.
Antes de começar, confirme que estas peças estão no lugar:
- Sua aplicação tem um
composer.jsone umcomposer.lockcomitados, comnextpdf/corecomo dependência. - Você tem os arquivos de fonte que pretende incorporar, e você está licenciado para incorporá-los.
- Você consegue rodar
docker buildcontra o diretório da sua aplicação.
Este é um how-to de operações. Quase não há PHP aqui; o trabalho é o Dockerfile e algumas configurações de ambiente.
O que o engine realmente exige
Seção intitulada “O que o engine realmente exige”A imagem deve satisfazer as restrições reais de plataforma do engine, nada mais.
Lendo-as direto do pacote, nextpdf/core exige
php: >=8.4 <9.0 e estas extensões PHP:
| Extensão | Por que o engine precisa dela |
|---|---|
ext-mbstring | Manipulação de strings multibyte para texto e codificações |
ext-intl | Suporte a Unicode, locale e internacionalização |
ext-gd | Decodificação e processamento de imagens raster |
ext-openssl | Criptografia para assinatura e hashing seguro |
ext-zlib | Compressão de stream (Flate) de objetos PDF |
ext-curl | Cliente HTTP para as chamadas de saída do engine |
Mapeie-as para a imagem oficial php:8.4. openssl, curl e zlib já estão
compiladas na imagem PHP oficial, então você não as instala com
docker-php-ext-install. mbstring, gd e intl não estão embutidas e
precisam ser instaladas, e cada uma precisa de seus headers de desenvolvimento de
sistema presentes primeiro — mbstring precisa adicionalmente da dependência de
build libonig-dev (Oniguruma). Não adicione extensões de engine
que o pacote não lista — cada docker-php-ext-install extra é tempo de build
e superfície de ataque de que você não precisa. A única extensão não-engine que esta
imagem instala é o opcache: ele é uma extensão de performance de runtime, não
vem habilitada na imagem oficial, e o ajuste de opcache abaixo depende de ele estar
presente (veja “Opcache para produção”).
O Dockerfile de produção
Seção intitulada “O Dockerfile de produção”Este é um build de dois estágios. O primeiro estágio instala as dependências do Composer com os pacotes de desenvolvimento excluídos; o segundo estágio é a imagem de runtime enxuta que é enviada.
Primeiro adicione um .dockerignore ao lado do Dockerfile. Sua função principal é
manter o ambiente do host — um vendor/ construído no host, arquivos de segredos
locais e caches de build — totalmente fora do contexto de build, para que
COPY . /var/www/app envie apenas o que você pretende: builds menores, mais rápidos
e mais seguros que não podem vazar segredos locais de .env nem carregar megabytes
do vendor/ do host para dentro da imagem.
Excluir vendor/ também importa porque um COPY de diretório é um merge, não
um replace. O Dockerfile abaixo roda RUN rm -rf /var/www/app/vendor antes do
COPY --from=vendor ... /var/www/app/vendor, então em esta imagem um vendor/ do
host nunca pode sobreviver sob a árvore de dependências limpa. Mas se você um dia
remover essa salvaguarda rm -rf, um vendor/ construído no host no contexto
chegaria primeiro e a cópia do estágio vendor só sobrescreveria os caminhos que a
árvore limpa contém — quaisquer arquivos extras do host (um pacote obsoleto ou
instalado em dev, uma classe órfã) sobreviveriam por baixo dela. Manter vendor/
fora do contexto fecha esse buraco independentemente do rm -rf.
# .dockerignore — keep the host environment out of the build context.vendor/.git/.env.env.local.env.*.localvar/cache/storage/node_modules/*.logExclua os arquivos de segredos locais reais (.env, .env.local, .env.*.local),
não um .env.* genérico — esse curinga também descarta templates não secretos como
.env.example que você quer enviar para que a imagem carregue uma baseline de
configuração documentada. Mantenha no contexto qualquer template de env comitado e
não secreto; exclua apenas os arquivos que realmente contêm segredos locais.
# 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"]O estágio de dependências roda com --no-scripts para que nenhum hook de
pós-instalação da aplicação rode contra uma árvore incompleta; rode qualquer etapa
de build da aplicação (compilação de assets, aquecimento de cache) em um estágio
posterior, depois que o código for copiado.
Instalação Composer multi-estágio (sem dependências de dev)
Seção intitulada “Instalação Composer multi-estágio (sem dependências de dev)”A imagem enviada não pode conter ferramental de desenvolvimento. A flag --no-dev
no composer install é a linha que sustenta tudo: ela pula tudo sob require-dev
no nextpdf/core e na sua aplicação — o test runner, o analisador estático e as
ferramentas de mutação — nenhum dos quais tem lugar em produção. Combine-a com
--optimize-autoloader para que o autoloader seja um class map gerado em vez de uma
varredura de sistema de arquivos a cada requisição.
Copie composer.json e composer.lock antes do restante da fonte para que o
Docker faça cache da camada de dependências e só re-resolva quando o arquivo de lock
muda. Como essa primeira instalação roda apenas contra o arquivo de lock — sem a
fonte da aplicação — o --optimize-autoloader ali constrói o class map para a
árvore vendor apenas; as classes da sua própria aplicação ainda não estão
presentes. É por isso que o estágio de runtime roda
composer dump-autoload --optimize --no-dev --no-scripts uma vez depois de
copiar a fonte: ele dobra as classes do app no mesmo class map otimizado. Não rode
um composer dump-autoload separado em uma worktree em que você também desenvolve
(isso comitaria um class map de produção em uma árvore de dev); o rebuild pertence à
imagem, depois da cópia da fonte, como mostrado acima.
Empacote fontes na imagem
Seção intitulada “Empacote fontes na imagem”O engine nativo resolve fontes a partir de arquivos de fonte que ele consegue
ler, não de fontes instaladas pelo sistema operacional. Instalar pacotes fonts-*
ou rodar fc-cache não faz nada que o caminho nativo enxergue, então esta imagem
não instala nenhuma fonte de sistema. Empacote seus arquivos .ttf / .otf sob
resources/fonts/; o COPY . /var/www/app acima já os carrega para a imagem.
Colocar os arquivos na imagem é só metade do trabalho. O engine nativo puro
lê nenhuma variável de ambiente de busca de fontes — NEXTPDF_FONTS_PATH é o
valor padrão da chave de config 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. Um entrypoint simples
php bin/generate.php com apenas essa variável definida não registra nenhuma fonte
e renderiza o mesmo tofu que esta imagem existe para prevenir. O entrypoint precisa
registrar o diretório empacotado em 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();Essa é toda a preocupação do Docker com fontes. As regras de nomenclatura de arquivos, a API do registro, o padrão warmup-and-lock e o tratamento de sistema de arquivos somente leitura ficam todos na página dedicada — não os duplique aqui. Leia Provisione fontes para o engine nativo em produção para o padrão completo, e registre o mesmo diretório que você empacotou.
Rode como um usuário não-root
Seção intitulada “Rode como um usuário não-root”As imagens PHP oficiais rodam como root por padrão. Um gerador de PDF não precisa
de root, então crie um usuário sem privilégios e mude para ele. O Dockerfile acima
adiciona um usuário de sistema appuser com um UID alto e fixo (10001), dá a ele
a propriedade da árvore da aplicação e termina com USER appuser para que cada
processo que o contêiner inicia seja sem privilégios.
Mantenha a aplicação somente leitura em runtime onde puder. O engine lê seus
arquivos de fonte e escreve apenas sua saída e um cache opcional de fontes
analisadas, então um contêiner readOnlyRootFilesystem funciona desde que o caminho
de saída e qualquer diretório de cache sejam mounts graváveis. Combine isto com
capabilities do Linux removidas e uma flag no-new-privileges no seu orquestrador
para defesa em profundidade.
Opcache para produção
Seção intitulada “Opcache para produção”O opcache compensa para workers PHP de longa duração — um pool FPM ou um
processo mod_php do Apache que serve muitas requisições a partir de um processo
quente. Esses processos compilam suas classes uma vez e depois nunca mais fazem stat
de arquivos de fonte em um caminho quente, que é exatamente o que
opcache.validate_timestamps=0 compra para você. O opcache não vem habilitado
de fábrica na imagem oficial php:8.4, então o Dockerfile acima o instala com
docker-php-ext-install opcache (você pode equivalentemente
docker-php-ext-enable opcache se a extensão já estiver compilada). O arquivo
conf.d abaixo é ajuste, não a etapa de habilitação — ele não faz nada até a
extensão ser carregada. Envie-o como um include conf.d (docker/opcache.ini,
copiado no 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 o cache nunca re-checa os arquivos de
fonte — correto para uma imagem imutável, já que a única forma de o código mudar é
uma nova imagem. Ajuste memory_consumption e max_accelerated_files para a
contagem de classes da sua aplicação.
O CMD mostrado é um gerador CLI de execução única, e opcache.enable_cli=0 está
correto para ele. Um processo php bin/generate.php de vida curta inicia, compila,
renderiza uma vez e sai, então um cache de opcode que ele não consegue compartilhar
com uma próxima requisição não dá benefício — deixe o opcache CLI desligado e não
pague nada do seu custo de memória. O opcache só vale a pena onde o processo é
reutilizado: uma SAPI FPM/Apache, ou um worker CLI genuinamente de longa duração (um
consumidor de fila ou um servidor estilo RoadRunner). Apenas esse tipo de worker CLI
residente definiria opcache.enable_cli=1; para o gerador de execução única aqui,
mantenha-o em 0.
Se você de fato rodar uma configuração que usa preloading de opcache (um worker
FPM de longa duração com um script opcache.preload), defina
opcache.preload=/path/to/preload.php e adicione opcache.preload_user=appuser
para que o preload rode como o usuário sem privilégios. Sem um script
opcache.preload de verdade, opcache.preload_user não faz nada, que é por que ele
não está no config baseline acima — não o adicione a menos que você também defina
opcache.preload.
Verifique a imagem
Seção intitulada “Verifique a imagem”Adicione uma etapa de verificação para que uma imagem mal construída falhe
ruidosamente em vez de produzir tofu ou um fatal na primeira requisição. O NextPDF
inclui uma CLI cujo comando doctor inspeciona o ambiente PHP em execução e reporta
exatamente as extensões com que o engine se importa — openssl, zlib,
mbstring, gd, curl e intl. O pacote declara "bin": ["bin/nextpdf"], então
em uma aplicação consumidora o Composer instala o executável em vendor/bin/nextpdf
(não bin/nextpdf, que é o caminho dentro do próprio pacote nextpdf/core).
Execute-o dentro da imagem construída:
docker run --rm your-app:latest php vendor/bin/nextpdf doctorUm resultado saudável confirma o PHP 8.4 e que cada extensão exigida está carregada. Conecte a mesma chamada ao build (ou a um job de smoke do CI) para que uma extensão ausente pare o pipeline:
# Fail the pipeline if the engine's environment is not healthy.docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1Para uma verificação ponta a ponta, renderize uma página pelo seu próprio entrypoint e afirme sobre a saída, como a página de fontes descreve para uma verificação de smoke de fonte.
Casos extremos e pegadinhas
Seção intitulada “Casos extremos e pegadinhas”php:8.4-fpmou-apacheem vez de-cli. Use a SAPI sob a qual o seu app realmente serve. A lista de extensões é idêntica; apenas a tag base e oCMD/entrypoint diferem. Para um worker de fila ou um job CLI em lote,-cliestá correto.- Alpine (
php:8.4-alpine) precisa de nomes de pacote diferentes. As linhasapt-getacima são para a imagem padrão baseada em Debian. No Alpine, instale os headers*-devcomo um grupo de build virtual (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) e, depois da etapadocker-php-ext-install gd intl mbstring opcache,apk del .build-deps— mas primeiroapk add --no-cacheas bibliotecas de runtime contra as quais as extensões fazem link (icu-libs,libpng,freetype,libjpeg-turbo,oniguruma) para que deletar o grupo de build não desfaça o link deintl.so/gd.so/mbstring.so. Esta é a mesma regra de manter-as-libs-de-runtime que o bloco Debian impõe comapt-mark. - Não instale pacotes
fonts-*. Eles são invisíveis para o engine nativo. Empacote arquivos de fonte em vez disso — veja a página de fontes linkada acima. - Premium e ionCube são uma preocupação de imagem diferente. As builds NextPDF Pro / Enterprise codificadas com ionCube precisam do ionCube Loader instalado na imagem e correspondendo ao build de PHP exato do contêiner (8.4, NTS vs. ZTS). Isso está fora de escopo para uma imagem core; se você implanta o premium, siga a seção Docker de Configuração do ionCube Loader.
- Mantenha um
vendor/do host fora do contexto de build. O.dockerignore(excluindovendor/,.git/e caches locais) mantém a árvore do host fora do contexto inteiramente — é isso que torna o build pequeno, rápido e livre de segredos locais vazados. Ele também protege o caso de merge de diretório: umCOPYde diretório é um merge, não um replace, então umvendor/construído no host que alcançasse o contexto chegaria primeiro e oCOPY --from=vendor /app/vendor /var/www/app/vendorsó sobrescreveria os caminhos que a árvore de dependências limpa contém. Neste Dockerfile, oRUN rm -rf /var/www/app/vendorantes da cópia do vendor já remove qualquer diretório desse tipo, então esse resíduo não pode ocorrer aqui; o risco de merge só retorna se você abandonar essa salvaguardarm -rf, que é por que a exclusão do.dockerignoreé a correção durável.
Notas de segurança
Seção intitulada “Notas de segurança”- Não envie dependências de dev.
--no-devmantém o ferramental de teste e análise, e seus pacotes transitivos, fora da imagem de runtime e da sua superfície de ataque. - Rode sem privilégios. O
USER appuserfinal garante que nenhum processo do contêiner rode como root. Combine-o com um sistema de arquivos raiz somente leitura e capabilities removidas no seu orquestrador. - Fixe a imagem base. Fixe
php:8.4a um digest em produção para que um rebuild não possa puxar silenciosamente uma base alterada, e reconstrua com uma cadência para incorporar patches de segurança deliberadamente. - Mantenha fontes e licenças fora de camadas públicas. Empacote apenas fontes que você está licenciado para incorporar, e nunca embuta um arquivo de licença premium em uma imagem publicada publicamente — monte-o em runtime em vez disso.
Conformidade
Seção intitulada “Conformidade”Este guia não faz nenhuma afirmação normativa de padrões. Os fatos de plataforma são
lidos diretamente do pacote nextpdf/core: a restrição php: >=8.4 <9.0 e
as extensões exigidas ext-mbstring, ext-intl, ext-gd, ext-openssl,
ext-zlib e ext-curl. O comando de verificação é o handler doctor real da CLI
nextpdf — declarado como "bin": ["bin/nextpdf"] em nextpdf/core e
portanto instalado em vendor/bin/nextpdf em um app consumidor — que reporta sobre
o mesmo conjunto de extensões. O engine nativo
registra fontes por meio de NextPDF\Typography\FontRegistry (o argumento de
diretório do construtor / addFontDirectory()) conectado via
NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH é a chave de config
fonts_path do pacote nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), não uma variável que nextpdf/core lê. O comportamento
do registro está documentado na página de fontes linkada em Veja também.
Veja também
Seção intitulada “Veja também”- Provisione fontes para o engine nativo em produção: a nomenclatura de arquivos de fonte, a API do registro e o padrão warmup-and-lock em que esta imagem se apoia.
- Faça streaming de um PDF grande gerado como resposta HTTP: o modelo de memória para servir um documento construído a partir de um controller de framework.
- Renderize na edge com o Cloudflare: quando um contêiner no mesmo processo não é o runtime certo.
- Configuração do ionCube Loader: a preocupação de imagem separada para builds premium codificadas com ionCube.