Containerizzare un'applicazione NextPDF
In sintesi
Sezione intitolata “In sintesi”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.jsone uncomposer.lockcommittati, connextpdf/corecome dipendenza. - Si dispone dei file di font che si intende incorporare e si è autorizzati a incorporarli.
- È possibile eseguire
docker buildsulla 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.
Che cosa richiede effettivamente il motore
Sezione intitolata “Che cosa richiede effettivamente il motore”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:
| Estensione | Perché il motore ne ha bisogno |
|---|---|
ext-mbstring | Gestione di stringhe multi-byte per testo e codifiche |
ext-intl | Supporto Unicode, locale e internazionalizzazione |
ext-gd | Decodifica ed elaborazione di immagini raster |
ext-openssl | Crittografia per la firma e l’hashing sicuro |
ext-zlib | Compressione stream (Flate) degli oggetti PDF |
ext-curl | Client 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”).
Il Dockerfile di produzione
Sezione intitolata “Il Dockerfile di 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.*.localvar/cache/storage/node_modules/*.logEscludere 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 /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"]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.
Includere i font nell’immagine
Sezione intitolata “Includere i font nell’immagine”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.
Eseguire come utente non-root
Sezione intitolata “Eseguire come utente non-root”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 per la produzione
Sezione intitolata “Opcache per la produzione”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=1opcache.enable_cli=0opcache.memory_consumption=192opcache.interned_strings_buffer=16opcache.max_accelerated_files=20000opcache.validate_timestamps=0opcache.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.
Verificare l’immagine
Sezione intitolata “Verificare l’immagine”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:
docker run --rm your-app:latest php vendor/bin/nextpdf doctorUn 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:
# Fail the pipeline if the engine's environment is not healthy.docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1Per 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.
Casi limite e insidie
Sezione intitolata “Casi limite e insidie”php:8.4-fpmo-apacheanziché-cli. Usare il SAPI sotto cui la propria app è effettivamente servita. L’elenco delle estensioni è identico; differiscono soltanto il tag di base e ilCMD/entrypoint. Per un worker di coda o un job CLI batch,-cliè corretto.- Alpine (
php:8.4-alpine) richiede nomi di pacchetto diversi. Le righeapt-getqui sopra sono per l’immagine predefinita basata su Debian. Su Alpine, installare gli header*-devcome 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 passaggiodocker-php-ext-install gd intl mbstring opcache,apk del .build-deps— ma prima eseguireapk add --no-cacheper 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 scolleghiintl.so/gd.so/mbstring.so. Questa è la stessa regola di mantenimento delle librerie di runtime che il blocco Debian impone conapt-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 escludevendor/,.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: unCOPYdi directory è una fusione, non una sostituzione, perciò unvendor/costruito sull’host che raggiungesse il contesto atterrerebbe per primo e ilCOPY --from=vendor /app/vendor /var/www/app/vendorsovrascriverebbe soltanto i percorsi che l’albero di dipendenze pulito contiene. In questo Dockerfile ilRUN rm -rf /var/www/app/vendorprima 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 protezionerm -rf, motivo per cui l’esclusione.dockerignoreè la correzione durevole.
Note sulla sicurezza
Sezione intitolata “Note sulla sicurezza”- Non distribuire dipendenze dev.
--no-devtiene 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 appuserfinale 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.4a 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.
Conformità
Sezione intitolata “Conformità”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.
Vedere anche
Sezione intitolata “Vedere anche”- Fornire i font al motore nativo in produzione: la denominazione dei file di font, l’API del registro e il pattern di scaldamento-e-blocco su cui si basa questa immagine.
- Trasmettere in streaming un grande PDF generato come risposta HTTP: il modello di memoria per servire un documento costruito da un controller di framework.
- Rendere all’edge con Cloudflare: quando un container in-process non è il runtime adatto.
- Configurazione del ionCube Loader: la questione di immagine separata per le build premium codificate con ionCube.