Aller au contenu
getnextpdf.com

Conteneuriser une application NextPDF

Tu veux une image Docker petite et reproductible qui exécute le moteur de cœur NextPDF natif, en processuscomposer 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.json et un composer.lock committés, avec nextpdf/core comme 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 build contre 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.

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 :

ExtensionPourquoi le moteur en a besoin
ext-mbstringGestion des chaînes multi-octets pour le texte et les encodages
ext-intlPrise en charge Unicode, locale et internationalisation
ext-gdDécodage et traitement des images matricielles
ext-opensslCryptographie pour la signature et le hachage sécurisé
ext-zlibCompression de flux (Flate) des objets PDF
ext-curlClient 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 »).

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

Exclus 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 /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"]

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.

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

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

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 :

Fenêtre de terminal
docker run --rm your-app:latest php vendor/bin/nextpdf doctor

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

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

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

  • php:8.4-fpm ou -apache au 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 le CMD/point d’entrée diffèrent. Pour un worker de file d’attente ou une tâche par lots CLI, -cli est correct.
  • Alpine (php:8.4-alpine) a besoin de noms de paquets différents. Les lignes apt-get ci-dessus sont pour l’image par défaut basée sur Debian. Sur Alpine, installe les en-têtes *-dev comme 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’étape docker-php-ext-install gd intl mbstring opcache, apk del .build-deps — mais d’abord apk add --no-cache les 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 pas intl.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 avec apt-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 exclut vendor/, .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 : un COPY de répertoire est une fusion, pas un remplacement, donc un vendor/ construit sur l’hôte qui aurait atteint le contexte atterrirait en premier et le COPY --from=vendor /app/vendor /var/www/app/vendor n’écraserait que les chemins que l’arbre de dépendances propre contient. Dans ce Dockerfile, le RUN rm -rf /var/www/app/vendor avant 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-fou rm -rf, c’est pourquoi l’exclusion du .dockerignore est le correctif durable.
  • Ne livre aucune dépendance de dev. --no-dev garde 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 appuser final 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.

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.