Pular para o conteúdo
getnextpdf.com

Coloque uma aplicação NextPDF em contêiner

Você quer uma imagem Docker pequena e reproduzível que rode o engine NextPDF core nativo, no mesmo processocomposer 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.json e um composer.lock comitados, com nextpdf/core como dependência.
  • Você tem os arquivos de fonte que pretende incorporar, e você está licenciado para incorporá-los.
  • Você consegue rodar docker build contra 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.

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ãoPor que o engine precisa dela
ext-mbstringManipulação de strings multibyte para texto e codificações
ext-intlSuporte a Unicode, locale e internacionalização
ext-gdDecodificação e processamento de imagens raster
ext-opensslCriptografia para assinatura e hashing seguro
ext-zlibCompressão de stream (Flate) de objetos PDF
ext-curlCliente 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”).

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.*.local
var/cache/
storage/
node_modules/
*.log

Exclua 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 /app
COPY 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/app
RUN rm -rf /var/www/app/vendor
COPY --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/app
USER 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.

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.

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.

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=1
opcache.enable_cli=0
opcache.memory_consumption=192
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

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

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:

Terminal window
docker run --rm your-app:latest php vendor/bin/nextpdf doctor

Um 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:

Terminal window
# Fail the pipeline if the engine's environment is not healthy.
docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1

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

  • php:8.4-fpm ou -apache em 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 o CMD/entrypoint diferem. Para um worker de fila ou um job CLI em lote, -cli está correto.
  • Alpine (php:8.4-alpine) precisa de nomes de pacote diferentes. As linhas apt-get acima são para a imagem padrão baseada em Debian. No Alpine, instale os headers *-dev como 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 etapa docker-php-ext-install gd intl mbstring opcache, apk del .build-deps — mas primeiro apk add --no-cache as 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 de intl.so / gd.so / mbstring.so. Esta é a mesma regra de manter-as-libs-de-runtime que o bloco Debian impõe com apt-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 (excluindo vendor/, .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: um COPY de diretório é um merge, não um replace, então um vendor/ construído no host que alcançasse o contexto chegaria primeiro e o COPY --from=vendor /app/vendor /var/www/app/vendor só sobrescreveria os caminhos que a árvore de dependências limpa contém. Neste Dockerfile, o RUN rm -rf /var/www/app/vendor antes 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 salvaguarda rm -rf, que é por que a exclusão do .dockerignore é a correção durável.
  • Não envie dependências de dev. --no-dev manté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 appuser final 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.4 a 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.

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.