Conteneuriser une application NextPDF
En un coup d’œil
Section intitulée « En un coup d’œil »Tu veux une image Docker petite et reproductible qui exécute le moteur de cœur
NextPDF natif, en processus — composer require nextpdf/core, générant des PDF à
l’intérieur de ton processus PHP. Cette page construit exactement cela : une image
php:8.4 avec seulement les extensions dont le moteur a réellement besoin, aucune
dépendance de développement dans la couche finale, des polices empaquetées, un
utilisateur d’exécution non-root, opcache réglé pour la production, et une étape de
vérification qui fait échouer le build si quelque chose manque.
Cette page est uniquement pour le moteur natif. Le pont Chrome
(writeHtmlChrome via nextpdf/artisan) et le serveur Connect sont des runtimes
distincts avec leurs propres images, plus lourdes — une installation de Chromium
headless pour le pont, un service de longue durée pour Connect. N’ajoute pas de
navigateur ni de serveur à cette image ; le moteur natif n’a besoin ni de l’un ni de
l’autre.
Avant de commencer, confirme que ces éléments sont en place :
- Ton application possède un
composer.jsonet uncomposer.lockcommittés, avecnextpdf/corecomme dépendance. - Tu disposes des fichiers de polices que tu comptes embarquer, et tu as le droit de les embarquer.
- Tu peux exécuter
docker buildcontre le répertoire de ton application.
C’est un mode d’emploi d’exploitation. Il n’y a presque pas de PHP ici ; le travail, c’est le Dockerfile et quelques réglages d’environnement.
Ce que le moteur exige réellement
Section intitulée « Ce que le moteur exige réellement »L’image doit satisfaire les vraies contraintes de plateforme du moteur, rien de
plus. En les lisant directement dans le paquet, nextpdf/core exige
php: >=8.4 <9.0 et ces extensions PHP :
| Extension | Pourquoi le moteur en a besoin |
|---|---|
ext-mbstring | Gestion des chaînes multi-octets pour le texte et les encodages |
ext-intl | Prise en charge Unicode, locale et internationalisation |
ext-gd | Décodage et traitement des images matricielles |
ext-openssl | Cryptographie pour la signature et le hachage sécurisé |
ext-zlib | Compression de flux (Flate) des objets PDF |
ext-curl | Client HTTP pour les appels sortants du moteur |
Mappe-les sur l’image officielle php:8.4. openssl, curl et zlib sont déjà
compilés dans l’image PHP officielle, donc tu ne les docker-php-ext-install pas.
mbstring, gd et intl ne sont pas fournis et doivent être installés, et
chacun a besoin que ses en-têtes de développement système soient présents au
préalable — mbstring a en plus besoin de la dépendance de build libonig-dev
(Oniguruma). N’ajoute pas d’extensions de moteur que le paquet ne liste pas — chaque
docker-php-ext-install supplémentaire est du temps de build et de la surface
d’attaque dont tu n’as pas besoin. La seule extension hors moteur que cette image
installe est opcache : c’est une extension de performance d’exécution, non
fournie activée sur l’image officielle, et le réglage opcache ci-dessous dépend de sa
présence (voir « Opcache pour la production »).
Le Dockerfile de production
Section intitulée « Le Dockerfile de production »C’est un build à deux étapes. La première étape installe les dépendances Composer en excluant les paquets de développement ; la seconde étape est l’image d’exécution allégée qui est livrée.
Ajoute d’abord un .dockerignore à côté du Dockerfile. Son rôle premier est de garder
l’environnement de l’hôte — un vendor/ construit sur l’hôte, des fichiers de secrets
locaux et des caches de build — entièrement hors du contexte de build, pour que
COPY . /var/www/app ne livre que ce que tu veux : des builds plus petits, plus
rapides et plus sûrs qui ne peuvent pas fuiter des secrets .env locaux ni emporter
des mégaoctets de vendor/ de l’hôte dans l’image.
Exclure vendor/ compte aussi parce qu’un COPY de répertoire est une fusion,
pas un remplacement. Le Dockerfile ci-dessous exécute RUN rm -rf /var/www/app/vendor
avant le COPY --from=vendor ... /var/www/app/vendor, donc dans cette image un
vendor/ de l’hôte ne peut jamais survivre sous l’arbre de dépendances propre. Mais
si tu supprimes un jour ce garde-fou rm -rf, un vendor/ construit sur l’hôte
présent dans le contexte atterrirait en premier et la copie de l’étape vendor
n’écraserait que les chemins que l’arbre propre contient — tout fichier d’hôte
supplémentaire (un paquet périmé ou installé en dev, une classe orpheline)
survivrait alors en dessous. Garder vendor/ hors du contexte ferme ce trou quel que
soit le rm -rf.
# .dockerignore — keep the host environment out of the build context.vendor/.git/.env.env.local.env.*.localvar/cache/storage/node_modules/*.logExclus les vrais fichiers de secrets locaux (.env, .env.local, .env.*.local),
pas un .env.* global — ce joker écarte aussi des modèles non secrets tels que
.env.example que tu veux livrer pour que l’image porte une base de configuration
documentée. Garde dans le contexte tout modèle d’environnement committé et non
secret ; n’exclus que les fichiers qui contiennent réellement des secrets locaux.
# 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"]L’étape de dépendances s’exécute avec --no-scripts pour qu’aucun hook applicatif de
post-installation ne tourne contre un arbre incomplet ; exécute toute étape de build
applicative (compilation des actifs, préchauffage de cache) dans une étape ultérieure
après la copie du code.
Installation Composer multi-étapes (sans dépendances de dev)
Section intitulée « Installation Composer multi-étapes (sans dépendances de dev) »L’image livrée ne doit pas contenir d’outillage de développement. Le drapeau
--no-dev sur composer install est la ligne porteuse : il saute tout ce qui se
trouve sous require-dev dans nextpdf/core et ton application — le lanceur de
tests, l’analyseur statique et les outils de mutation — dont aucun n’a sa place en
production. Associe-le à --optimize-autoloader pour que l’autoloader soit une class
map générée plutôt qu’un balayage du système de fichiers à chaque requête.
Copie composer.json et composer.lock avant le reste de la source pour que
Docker mette en cache la couche de dépendances et ne re-résolve que lorsque le fichier
de lock change. Parce que cette première installation tourne contre le seul fichier de
lock — sans la source applicative — --optimize-autoloader y construit la class map
pour l’arbre vendor uniquement ; les classes propres à ton application ne sont pas
encore présentes. C’est pourquoi l’étape d’exécution lance
composer dump-autoload --optimize --no-dev --no-scripts une fois après la copie
de la source : elle replie les classes de l’application dans la même class map
optimisée. N’exécute pas un composer dump-autoload séparé dans un worktree où tu
développes aussi (cela committerait une class map de production dans un arbre de dev) ;
la reconstruction appartient à l’image, après la copie de la source, comme montré
ci-dessus.
Empaqueter les polices dans l’image
Section intitulée « Empaqueter les polices dans l’image »Le moteur natif résout les polices à partir de fichiers de polices qu’il peut
lire, pas à partir des polices installées par l’OS. Installer des paquets fonts-*
ou exécuter fc-cache ne fait rien que le chemin natif puisse voir, donc cette image
n’installe aucune police système. Empaquette tes fichiers .ttf / .otf sous
resources/fonts/ ; le COPY . /var/www/app ci-dessus les emporte déjà dans
l’image.
Mettre les fichiers dans l’image n’est que la moitié du travail. Le moteur natif nu
ne lit aucune variable d’environnement de recherche de polices —
NEXTPDF_FONTS_PATH est la valeur par défaut de la clé de config fonts_path du
paquet nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) et
n’est consommée que par cette intégration de framework, pas par nextpdf/core. Un
simple point d’entrée php bin/generate.php avec seulement cette variable définie
n’enregistre aucune police et rend le même tofu que cette image existe pour empêcher.
Le point d’entrée doit enregistrer le répertoire empaqueté en 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();C’est là toute la préoccupation Docker concernant les polices. Les règles de nommage des fichiers, l’API du registre, le motif chauffe-et-verrouille et la gestion du système de fichiers en lecture seule vivent tous sur la page dédiée — ne les duplique pas ici. Lis Provisionner les polices pour le moteur natif en production pour le motif complet, et enregistre le même répertoire que celui que tu as empaqueté.
Exécuter en tant qu’utilisateur non-root
Section intitulée « Exécuter en tant qu’utilisateur non-root »Les images PHP officielles s’exécutent en tant que root par défaut. Un générateur de
PDF n’a pas besoin de root, donc crée un utilisateur sans privilège et bascule vers
lui. Le Dockerfile ci-dessus ajoute un utilisateur système appuser avec un UID élevé
fixe (10001), lui donne la propriété de l’arbre applicatif, et se termine par
USER appuser pour que tout processus que le conteneur démarre soit sans privilège.
Garde l’application en lecture seule à l’exécution là où tu le peux. Le moteur lit ses
fichiers de polices et n’écrit que sa sortie et un cache de polices analysées
optionnel, donc un conteneur readOnlyRootFilesystem fonctionne tant que le chemin de
sortie et tout répertoire de cache sont des montages inscriptibles. Combine cela avec
des capacités Linux supprimées et un drapeau no-new-privileges dans ton
orchestrateur pour une défense en profondeur.
Opcache pour la production
Section intitulée « Opcache pour la production »Opcache porte ses fruits pour les workers PHP de longue durée — un pool FPM ou un
processus Apache mod_php qui sert de nombreuses requêtes depuis un seul processus
chaud. Ces processus compilent tes classes une fois puis ne font plus jamais de
stat sur les fichiers source dans un chemin chaud, ce qui est exactement ce que
opcache.validate_timestamps=0 t’achète. Opcache n’est pas activé d’origine sur
l’image officielle php:8.4, donc le Dockerfile ci-dessus l’installe avec
docker-php-ext-install opcache (tu peux de façon équivalente
docker-php-ext-enable opcache si l’extension est déjà compilée). Le fichier conf.d
ci-dessous est du réglage, pas l’étape d’activation — il ne fait rien tant que
l’extension n’est pas chargée. Livre-le comme un include conf.d (docker/opcache.ini,
copié dans le Dockerfile) :
opcache.enable=1opcache.enable_cli=0opcache.memory_consumption=192opcache.interned_strings_buffer=16opcache.max_accelerated_files=20000opcache.validate_timestamps=0opcache.validate_timestamps=0 signifie que le cache ne re-vérifie jamais les
fichiers source — correct pour une image immuable, puisque la seule façon dont le code
change est une nouvelle image. Règle memory_consumption et max_accelerated_files
selon le nombre de classes de ton application.
Le CMD montré est un générateur CLI à coup unique, et opcache.enable_cli=0 est
correct pour lui. Un processus php bin/generate.php à courte durée démarre,
compile, rend une fois, et sort, donc un cache d’opcodes qu’il ne peut partager avec
aucune requête suivante n’apporte aucun bénéfice — laisse l’opcache CLI désactivé et
ne paie aucun de son coût mémoire. Opcache ne gagne sa place que là où le processus
est réutilisé : un SAPI FPM/Apache, ou un worker CLI véritablement de longue durée (un
consommateur de file d’attente ou un serveur de style RoadRunner). Seul ce genre de
worker CLI résident définirait opcache.enable_cli=1 ; pour le générateur à coup
unique ici, garde-le à 0.
Si tu exécutes une configuration qui utilise le préchargement opcache (un worker
FPM de longue durée avec un script opcache.preload), définis
opcache.preload=/path/to/preload.php et ajoute opcache.preload_user=appuser pour
que le préchargement s’exécute en tant qu’utilisateur sans privilège. Sans un vrai
script opcache.preload, opcache.preload_user ne fait rien, c’est pourquoi il n’est
pas dans la config de base ci-dessus — ne l’ajoute pas sauf si tu définis aussi
opcache.preload.
Vérifier l’image
Section intitulée « Vérifier l’image »Ajoute une étape de vérification pour qu’une image mal construite échoue bruyamment au
lieu de produire du tofu ou un fatal à la première requête. NextPDF livre une CLI dont
la commande doctor inspecte l’environnement PHP en cours d’exécution et rend compte
exactement des extensions qui importent au moteur — openssl, zlib, mbstring,
gd, curl et intl. Le paquet déclare "bin": ["bin/nextpdf"], donc dans une
application consommatrice Composer installe l’exécutable à vendor/bin/nextpdf
(pas bin/nextpdf, qui est le chemin à l’intérieur du paquet nextpdf/core
lui-même). Exécute-la dans l’image construite :
docker run --rm your-app:latest php vendor/bin/nextpdf doctorUn résultat sain confirme PHP 8.4 et que toute extension requise est chargée. Câble le même appel dans le build (ou une tâche de fumée en CI) pour qu’une extension manquante arrête le pipeline :
# Fail the pipeline if the engine's environment is not healthy.docker run --rm your-app:latest php vendor/bin/nextpdf doctor || exit 1Pour une vérification de bout en bout, rends une page via ton propre point d’entrée et fais une assertion sur la sortie, comme la page des polices le décrit pour une vérification de fumée de police.
Cas limites et pièges
Section intitulée « Cas limites et pièges »php:8.4-fpmou-apacheau lieu de-cli. Utilise le SAPI sous lequel ton application sert réellement. La liste d’extensions est identique ; seuls le tag de base et leCMD/point d’entrée diffèrent. Pour un worker de file d’attente ou une tâche par lots CLI,-cliest correct.- Alpine (
php:8.4-alpine) a besoin de noms de paquets différents. Les lignesapt-getci-dessus sont pour l’image par défaut basée sur Debian. Sur Alpine, installe les en-têtes*-devcomme un groupe de build virtuel (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) et, après l’étapedocker-php-ext-install gd intl mbstring opcache,apk del .build-deps— mais d’abordapk add --no-cacheles bibliothèques d’exécution contre lesquelles les extensions sont liées (icu-libs,libpng,freetype,libjpeg-turbo,oniguruma) pour que supprimer le groupe de build ne délie pasintl.so/gd.so/mbstring.so. C’est la même règle de garde-les-bibliothèques-d’exécution que le bloc Debian impose avecapt-mark. - N’installe pas de paquets
fonts-*. Ils sont invisibles au moteur natif. Empaquette plutôt des fichiers de polices — voir la page des polices liée ci-dessus. - Premium et ionCube sont une préoccupation d’image différente. Les builds NextPDF Pro / Enterprise encodés en ionCube ont besoin que le ionCube Loader soit installé dans l’image et apparié au build PHP exact du conteneur (8.4, NTS vs. ZTS). Cela sort du cadre d’une image de cœur ; si tu déploies du premium, suis la section Docker de Mise en place du ionCube Loader.
- Garde un
vendor/d’hôte hors du contexte de build. Le.dockerignore(qui exclutvendor/,.git/et les caches locaux) garde l’arbre de l’hôte entièrement hors du contexte — c’est ce qui rend le build petit, rapide et exempt de secrets locaux fuités. Il garde aussi contre le cas de la fusion de répertoires : unCOPYde répertoire est une fusion, pas un remplacement, donc unvendor/construit sur l’hôte qui aurait atteint le contexte atterrirait en premier et leCOPY --from=vendor /app/vendor /var/www/app/vendorn’écraserait que les chemins que l’arbre de dépendances propre contient. Dans ce Dockerfile, leRUN rm -rf /var/www/app/vendoravant la copie vendor supprime déjà tout répertoire de ce genre, donc ce résidu ne peut pas se produire ici ; le risque de fusion ne revient que si tu supprimes ce garde-fourm -rf, c’est pourquoi l’exclusion du.dockerignoreest le correctif durable.
Notes de sécurité
Section intitulée « Notes de sécurité »- Ne livre aucune dépendance de dev.
--no-devgarde l’outillage de test et d’analyse, et leurs paquets transitifs, hors de l’image d’exécution et de sa surface d’attaque. - Exécute sans privilège. Le
USER appuserfinal garantit qu’aucun processus du conteneur ne s’exécute en tant que root. Associe-le à un système de fichiers racine en lecture seule et à des capacités supprimées dans ton orchestrateur. - Épingle l’image de base. Épingle
php:8.4à un digest en production pour qu’une reconstruction ne puisse pas tirer silencieusement une base modifiée, et reconstruis à une cadence régulière pour récupérer délibérément les correctifs de sécurité. - Garde les polices et les licences hors des couches publiques. N’empaquette que les polices que tu as le droit d’embarquer, et ne cuis jamais un fichier de licence premium dans une image poussée publiquement — monte-le à l’exécution à la place.
Conformité
Section intitulée « Conformité »Ce guide ne fait aucune revendication normative de standard. Les faits de plateforme
sont lus directement dans le paquet nextpdf/core : la contrainte php: >=8.4 <9.0
et les extensions requises ext-mbstring, ext-intl, ext-gd, ext-openssl,
ext-zlib et ext-curl. La commande de vérification est le vrai gestionnaire
doctor de la CLI nextpdf — déclaré comme "bin": ["bin/nextpdf"] dans
nextpdf/core et donc installé à vendor/bin/nextpdf dans une application
consommatrice — qui rend compte du même jeu d’extensions. Le moteur natif enregistre
les polices via NextPDF\Typography\FontRegistry (l’argument répertoire du
constructeur / addFontDirectory()) câblé via NextPDF\Core\DocumentFactory ;
NEXTPDF_FONTS_PATH est la clé de config fonts_path du paquet nextpdf/laravel
(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), pas une variable que
nextpdf/core lit. Le comportement du registre est documenté sur la page des polices
liée sous Voir aussi.
Voir aussi
Section intitulée « Voir aussi »- Provisionner les polices pour le moteur natif en production : le nommage des fichiers de polices, l’API du registre et le motif chauffe-et-verrouille sur lesquels cette image s’appuie.
- Diffuser un gros PDF généré en réponse HTTP : le modèle mémoire pour servir un document construit depuis un contrôleur de framework.
- Rendre en bordure avec Cloudflare : quand un conteneur en processus n’est pas le bon runtime.
- Mise en place du ionCube Loader : la préoccupation d’image distincte pour les builds premium encodés en ionCube.