Salta ai contenuti
getnextpdf.com

Containerizzare un'applicazione NextPDF

Si desidera un’immagine Docker piccola e riproducibile che esegua il motore core nativo, in-process di NextPDF — composer require nextpdf/core, generando i PDF all’interno del proprio processo PHP. Questa pagina costruisce esattamente quello: un’immagine php:8.4 con solo le estensioni di cui il motore ha effettivamente bisogno, nessuna dipendenza di sviluppo nel layer finale, font inclusi, un utente di runtime non-root, opcache regolato per la produzione e un passaggio di verifica che fa fallire la build se manca qualcosa.

Questa pagina è solo per il motore nativo. Il bridge Chrome (writeHtmlChrome tramite nextpdf/artisan) e il server Connect sono runtime separati con immagini proprie e più pesanti — un’installazione di Chromium headless per il bridge, un servizio a lunga durata per Connect. Non aggiungere un browser o un server a questa immagine; il motore nativo non ha bisogno di nessuno dei due.

Prima di iniziare, confermare che questi elementi siano a posto:

  • La propria applicazione ha un composer.json e un composer.lock committati, con nextpdf/core come dipendenza.
  • Si dispone dei file di font che si intende incorporare e si è autorizzati a incorporarli.
  • È possibile eseguire docker build sulla directory della propria applicazione.

Questa è una guida pratica di tipo operativo. Qui c’è quasi nessun PHP; il lavoro è il Dockerfile e qualche impostazione d’ambiente.

L’immagine deve soddisfare i veri vincoli di piattaforma del motore, nulla di più. Leggendoli direttamente dal pacchetto, nextpdf/core richiede php: >=8.4 <9.0 e queste estensioni PHP:

EstensionePerché il motore ne ha bisogno
ext-mbstringGestione di stringhe multi-byte per testo e codifiche
ext-intlSupporto Unicode, locale e internazionalizzazione
ext-gdDecodifica ed elaborazione di immagini raster
ext-opensslCrittografia per la firma e l’hashing sicuro
ext-zlibCompressione stream (Flate) degli oggetti PDF
ext-curlClient HTTP per le chiamate in uscita del motore

Mappare queste sull’immagine ufficiale php:8.4. openssl, curl e zlib sono già compilate nell’immagine PHP ufficiale, perciò non si esegue docker-php-ext-install su di esse. mbstring, gd e intl non sono incluse e devono essere installate, e ciascuna richiede prima la presenza dei propri header di sviluppo di sistema — mbstring necessita inoltre della dipendenza di build libonig-dev (Oniguruma). Non aggiungere estensioni del motore che il pacchetto non elenca — ogni docker-php-ext-install extra è tempo di build e superficie di attacco di cui non si ha bisogno. L’unica estensione non-motore che questa immagine installa è opcache: è un’estensione di prestazioni a runtime, non inclusa abilitata nell’immagine ufficiale, e la regolazione di opcache qui sotto dipende dalla sua presenza (si veda “Opcache per la produzione”).

Questa è una build a due stage. Il primo stage installa le dipendenze Composer con i pacchetti di sviluppo esclusi; il secondo stage è l’immagine di runtime snella che viene distribuita.

Aggiungere prima un .dockerignore accanto al Dockerfile. Il suo compito primario è tenere l’ambiente host — un vendor/ costruito sull’host, file di segreti locali e cache di build — interamente fuori dal contesto di build, in modo che COPY . /var/www/app distribuisca solo ciò che si intende: build più piccole, più veloci e più sicure, che non possono far trapelare segreti .env locali né portare megabyte di vendor/ dell’host nell’immagine.

Escludere vendor/ conta anche perché un COPY di directory è una fusione, non una sostituzione. Il Dockerfile qui sotto esegue RUN rm -rf /var/www/app/vendor prima del COPY --from=vendor ... /var/www/app/vendor, perciò in questa immagine un vendor/ dell’host non può mai sopravvivere sotto l’albero di dipendenze pulito. Ma se si rimuovesse mai quella protezione rm -rf, un vendor/ costruito sull’host presente nel contesto atterrerebbe per primo e la copia dello stage vendor sovrascriverebbe soltanto i percorsi che l’albero pulito contiene — qualsiasi file host extra (un pacchetto obsoleto o installato come dev, una classe orfana) sopravvivrebbe quindi al di sotto di esso. Tenere vendor/ fuori dal contesto chiude quella falla a prescindere dall’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

Escludere i veri file di segreti locali (.env, .env.local, .env.*.local), non un generico .env.* — quel carattere jolly elimina anche i template non segreti come .env.example che si desidera distribuire affinché l’immagine porti una baseline di configurazione documentata. Mantenere nel contesto qualsiasi template d’ambiente committato e non segreto; escludere soltanto i file che contengono effettivamente segreti locali.

# 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"]

Lo stage delle dipendenze viene eseguito con --no-scripts in modo che nessun hook post-installazione dell’applicazione venga eseguito su un albero incompleto; eseguire qualsiasi passaggio di build dell’applicazione (compilazione degli asset, riscaldamento della cache) in uno stage successivo dopo che il codice è stato copiato.

Installazione Composer multi-stage (nessuna dipendenza dev)

Sezione intitolata “Installazione Composer multi-stage (nessuna dipendenza dev)”

L’immagine distribuita non deve contenere strumentazione di sviluppo. Il flag --no-dev su composer install è la riga portante: salta tutto ciò che è sotto require-dev in nextpdf/core e nella propria applicazione — il test runner, l’analizzatore statico e gli strumenti di mutazione — nessuno dei quali ha alcun posto in produzione. Abbinarlo a --optimize-autoloader in modo che l’autoloader sia una class map generata anziché una scansione del filesystem a ogni richiesta.

Copiare composer.json e composer.lock prima del resto del sorgente in modo che Docker memorizzi in cache il layer delle dipendenze e lo ri-risolva soltanto quando cambia il file di lock. Poiché quella prima installazione viene eseguita sul solo file di lock — senza il sorgente dell’applicazione — --optimize-autoloader lì costruisce la class map per il solo albero vendor; le classi della propria applicazione non sono ancora presenti. È per questo che lo stage di runtime esegue composer dump-autoload --optimize --no-dev --no-scripts una volta dopo aver copiato il sorgente: fonde le classi dell’app nella stessa class map ottimizzata. Non eseguire un composer dump-autoload separato in un worktree in cui si sviluppa anche (committerebbe una class map di produzione in un albero di sviluppo); la ricostruzione va fatta nell’immagine, dopo la copia del sorgente, come mostrato sopra.

Il motore nativo risolve i font a partire da file di font che è in grado di leggere, non dai font installati a livello di sistema operativo. Installare pacchetti fonts-* o eseguire fc-cache non produce nulla che il percorso nativo possa vedere, perciò questa immagine non installa alcun font di sistema. Includere i propri file .ttf / .otf sotto resources/fonts/; il COPY . /var/www/app qui sopra li porta già nell’immagine.

Portare i file nell’immagine è solo metà del lavoro. Il motore nativo nudo non legge alcuna variabile d’ambiente di ricerca dei font — NEXTPDF_FONTS_PATH è il valore predefinito della chiave di configurazione fonts_path del pacchetto nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) ed è consumata soltanto da quell’integrazione di framework, non da nextpdf/core. Un semplice entrypoint php bin/generate.php con solo quella variabile impostata non registra alcun font e rende lo stesso tofu che questa immagine esiste per prevenire. L’entrypoint deve registrare la directory inclusa in 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();

Questa è l’intera questione Docker per i font. Le regole di denominazione dei file, l’API del registro, il pattern di scaldamento-e-blocco e la gestione del filesystem di sola lettura risiedono tutti sulla pagina dedicata — non duplicarli qui. Leggere Fornire i font al motore nativo in produzione per il pattern completo, e registrare la stessa directory che si è inclusa.

Le immagini PHP ufficiali vengono eseguite come root per impostazione predefinita. Un generatore di PDF non ha bisogno di root, perciò creare un utente senza privilegi e passare a esso. Il Dockerfile qui sopra aggiunge un utente di sistema appuser con un UID alto fisso (10001), gli assegna la proprietà dell’albero dell’applicazione e termina con USER appuser in modo che ogni processo avviato dal container sia senza privilegi.

Mantenere l’applicazione di sola lettura a runtime dove possibile. Il motore legge i propri file di font e scrive soltanto il proprio output e una cache facoltativa dei font analizzati, perciò un container con readOnlyRootFilesystem funziona purché il percorso di output e qualsiasi directory di cache siano mount scrivibili. Combinare questo con capability Linux rimosse e un flag no-new-privileges nel proprio orchestratore per una difesa in profondità.

Opcache ripaga per i worker PHP a lunga durata — un pool FPM o un processo mod_php di Apache che serve molte richieste da un unico processo caldo. Quei processi compilano le proprie classi una volta e poi non eseguono mai lo stat dei file sorgente su un percorso caldo, che è esattamente ciò che acquista opcache.validate_timestamps=0. Opcache non è abilitato di default sull’immagine ufficiale php:8.4, perciò il Dockerfile qui sopra lo installa con docker-php-ext-install opcache (equivalentemente, è possibile usare docker-php-ext-enable opcache se l’estensione è già compilata). Il file conf.d qui sotto è regolazione, non il passaggio di abilitazione — non fa nulla finché l’estensione non è caricata. Distribuirlo come include conf.d (docker/opcache.ini, copiato nel 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 che la cache non ricontrolla mai i file sorgente — corretto per un’immagine immutabile, dato che l’unico modo in cui il codice cambia è una nuova immagine. Regolare memory_consumption e max_accelerated_files in base al numero di classi della propria applicazione.

Il CMD mostrato è un generatore CLI one-shot, e opcache.enable_cli=0 è corretto per esso. Un processo php bin/generate.php di breve durata si avvia, compila, rende una volta ed esce, perciò una cache di opcode che non può condividere con una richiesta successiva non offre alcun beneficio — lasciare l’opcache CLI disattivato e non pagarne alcun costo di memoria. Opcache guadagna il suo posto solo dove il processo viene riutilizzato: un SAPI FPM/Apache, o un worker CLI genuinamente a lunga durata (un consumatore di coda o un server in stile RoadRunner). Solo quel tipo di worker CLI residente imposterebbe opcache.enable_cli=1; per il generatore one-shot qui presente, mantenerlo a 0.

Se si esegue una configurazione che usa il preloading di opcache (un worker FPM a lunga durata con uno script opcache.preload), impostare opcache.preload=/path/to/preload.php e aggiungere opcache.preload_user=appuser in modo che il preload venga eseguito come utente senza privilegi. Senza un effettivo script opcache.preload, opcache.preload_user non fa nulla, motivo per cui non è nella configurazione di baseline qui sopra — non aggiungerlo a meno che non si imposti anche opcache.preload.

Aggiungere un passaggio di verifica in modo che un’immagine mal costruita fallisca in modo rumoroso anziché produrre tofu o un fatal alla prima richiesta. NextPDF distribuisce una CLI il cui comando doctor ispeziona l’ambiente PHP in esecuzione e riferisce esattamente sulle estensioni che interessano al motore — openssl, zlib, mbstring, gd, curl e intl. Il pacchetto dichiara "bin": ["bin/nextpdf"], perciò in un’applicazione consumatrice Composer installa l’eseguibile in vendor/bin/nextpdf (non bin/nextpdf, che è il percorso all’interno dello stesso pacchetto nextpdf/core). Eseguirlo all’interno dell’immagine costruita:

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

Un risultato sano conferma PHP 8.4 e che ogni estensione richiesta è caricata. Collegare la stessa chiamata nella build (o in un job smoke di CI) in modo che un’estensione mancante arresti la 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

Per un controllo end-to-end, rendere una pagina attraverso il proprio entrypoint e asserire sull’output, come descrive la pagina dei font per un controllo smoke dei font.

  • php:8.4-fpm o -apache anziché -cli. Usare il SAPI sotto cui la propria app è effettivamente servita. L’elenco delle estensioni è identico; differiscono soltanto il tag di base e il CMD/entrypoint. Per un worker di coda o un job CLI batch, -cli è corretto.
  • Alpine (php:8.4-alpine) richiede nomi di pacchetto diversi. Le righe apt-get qui sopra sono per l’immagine predefinita basata su Debian. Su Alpine, installare gli header *-dev come gruppo di build virtuale (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) e, dopo il passaggio docker-php-ext-install gd intl mbstring opcache, apk del .build-deps — ma prima eseguire apk add --no-cache per le librerie di runtime a cui le estensioni si collegano (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma) in modo che eliminare il gruppo di build non scolleghi intl.so / gd.so / mbstring.so. Questa è la stessa regola di mantenimento delle librerie di runtime che il blocco Debian impone con apt-mark.
  • Non installare pacchetti fonts-*. Sono invisibili al motore nativo. Includere invece i file di font — si veda la pagina dei font linkata sopra.
  • Premium e ionCube sono una questione di immagine diversa. Le build NextPDF Pro / Enterprise codificate con ionCube richiedono che il ionCube Loader sia installato nell’immagine e corrisponda all’esatta build PHP del container (8.4, NTS vs. ZTS). Questo esula dall’ambito di un’immagine core; se si distribuisce il premium, seguire la sezione Docker di Configurazione del ionCube Loader.
  • Tenere un vendor/ dell’host fuori dal contesto di build. Il .dockerignore (che esclude vendor/, .git/ e le cache locali) tiene l’albero host interamente fuori dal contesto — è ciò che rende la build piccola, veloce e priva di segreti locali trapelati. Tutela anche il caso di fusione di directory: un COPY di directory è una fusione, non una sostituzione, perciò un vendor/ costruito sull’host che raggiungesse il contesto atterrerebbe per primo e il COPY --from=vendor /app/vendor /var/www/app/vendor sovrascriverebbe soltanto i percorsi che l’albero di dipendenze pulito contiene. In questo Dockerfile il RUN rm -rf /var/www/app/vendor prima della copia vendor rimuove già qualsiasi directory di questo tipo, perciò quel residuo non può verificarsi qui; il rischio di fusione torna solo se si abbandona quella protezione rm -rf, motivo per cui l’esclusione .dockerignore è la correzione durevole.
  • Non distribuire dipendenze dev. --no-dev tiene la strumentazione di test e analisi, e i loro pacchetti transitivi, fuori dall’immagine di runtime e dalla sua superficie di attacco.
  • Eseguire senza privilegi. Il USER appuser finale garantisce che nessun processo del container venga eseguito come root. Abbinarlo a un filesystem root di sola lettura e a capability rimosse nel proprio orchestratore.
  • Fissare l’immagine di base. Fissare php:8.4 a un digest in produzione in modo che una ricostruzione non possa silenziosamente recuperare una base cambiata, e ricostruire con una cadenza per recepire le patch di sicurezza in modo deliberato.
  • Tenere font e licenze fuori dai layer pubblici. Includere solo font che si è autorizzati a incorporare, e non incorporare mai un file di licenza premium in un’immagine pubblicata pubblicamente — montarlo invece a runtime.

Questa guida non avanza alcuna affermazione normativa di conformità a standard. I fatti di piattaforma sono letti direttamente dal pacchetto nextpdf/core: il vincolo php: >=8.4 <9.0 e le estensioni richieste ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib ed ext-curl. Il comando di verifica è il vero handler doctor della CLI nextpdf — dichiarato come "bin": ["bin/nextpdf"] in nextpdf/core e perciò installato in vendor/bin/nextpdf in un’app consumatrice — che riferisce sullo stesso insieme di estensioni. Il motore nativo registra i font tramite NextPDF\Typography\FontRegistry (l’argomento del costruttore per la directory / addFontDirectory()) collegato tramite NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH è la chiave di configurazione fonts_path del pacchetto nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), non una variabile che nextpdf/core legge. Il comportamento del registro è documentato nella pagina dei font linkata sotto Vedere anche.