Zum Inhalt springen
getnextpdf.com

Eine NextPDF-Anwendung containerisieren

Sie möchten ein kleines, reproduzierbares Docker-Image, das die native, prozessinterne NextPDF-Core-Engine ausführt – composer require nextpdf/core, das PDFs innerhalb Ihres PHP-Prozesses erzeugt. Diese Seite baut genau das: ein php:8.4-Image mit nur den Erweiterungen, die die Engine tatsächlich braucht, ohne Entwicklungsabhängigkeiten in der finalen Schicht, mit gebündelten Schriften, einem Nicht-Root-Laufzeitbenutzer, für die Produktion abgestimmtem Opcache und einem Verifizierungsschritt, der den Build scheitern lässt, wenn etwas fehlt.

Diese Seite ist nur für die native Engine. Die Chrome-Bridge (writeHtmlChrome über nextpdf/artisan) und der Connect-Server sind separate Laufzeiten mit eigenen, schwereren Images – eine Headless-Chromium-Installation für die Bridge, ein langlaufender Dienst für Connect. Fügen Sie diesem Image weder einen Browser noch einen Server hinzu; die native Engine braucht keines von beidem.

Bestätigen Sie vor dem Start, dass diese Teile vorhanden sind:

  • Ihre Anwendung hat eine eingecheckte composer.json und composer.lock, mit nextpdf/core als Abhängigkeit.
  • Sie verfügen über die Schriftdateien, die Sie einbetten möchten, und Sie sind berechtigt, sie einzubetten.
  • Sie können docker build gegen Ihr Anwendungsverzeichnis ausführen.

Dies ist eine betriebliche Anleitung. Es gibt hier fast kein PHP; die Arbeit ist das Dockerfile und ein paar Umgebungseinstellungen.

Das Image muss die realen Plattform-Constraints der Engine erfüllen, nicht mehr. Direkt aus dem Paket gelesen, erfordert nextpdf/core php: >=8.4 <9.0 und diese PHP-Erweiterungen:

ErweiterungWarum die Engine sie braucht
ext-mbstringMultibyte-String-Verarbeitung für Text und Codierungen
ext-intlUnterstützung für Unicode, Locale und Internationalisierung
ext-gdDekodierung und Verarbeitung von Rasterbildern
ext-opensslKryptografie für Signieren und sicheres Hashing
ext-zlibStream-(Flate-)Komprimierung von PDF-Objekten
ext-curlHTTP-Client für die ausgehenden Aufrufe der Engine

Bilden Sie diese auf das offizielle php:8.4-Image ab. openssl, curl und zlib sind bereits in das offizielle PHP-Image kompiliert, sodass Sie sie nicht mit docker-php-ext-install installieren. mbstring, gd und intl sind nicht gebündelt und müssen installiert werden, und jede benötigt zuerst ihre System-Entwicklungs-Header – mbstring benötigt zusätzlich die libonig-dev-(Oniguruma-)Build-Abhängigkeit. Fügen Sie keine Engine-Erweiterungen hinzu, die das Paket nicht listet – jedes zusätzliche docker-php-ext-install ist Build-Zeit und Angriffsfläche, die Sie nicht brauchen. Die eine Nicht-Engine- Erweiterung, die dieses Image installiert, ist opcache: Sie ist eine Laufzeit-Performance-Erweiterung, im offiziellen Image nicht aktiviert gebündelt, und die Opcache-Abstimmung unten hängt davon ab, dass sie vorhanden ist (siehe „Opcache für die Produktion“).

Dies ist ein zweistufiger Build. Die erste Stufe installiert die Composer-Abhängigkeiten mit ausgeschlossenen Entwicklungspaketen; die zweite Stufe ist das schlanke Laufzeit-Image, das ausgeliefert wird.

Fügen Sie zuerst eine .dockerignore neben dem Dockerfile hinzu. Ihre primäre Aufgabe ist es, die Host-Umgebung – ein host-gebautes vendor/, lokale Geheimnisdateien und Build-Caches – vollständig aus dem Build-Kontext herauszuhalten, damit COPY . /var/www/app nur das ausliefert, was Sie beabsichtigen: kleinere, schnellere und sicherere Builds, die weder lokale .env-Geheimnisse durchsickern lassen noch Megabytes an Host-vendor/ ins Image tragen.

Das Ausschließen von vendor/ ist auch deshalb wichtig, weil ein Verzeichnis-COPY ein Merge ist, kein Ersetzen. Das Dockerfile unten führt RUN rm -rf /var/www/app/vendor vor dem COPY --from=vendor ... /var/www/app/vendor aus, sodass in diesem Image ein Host-vendor/ niemals unter dem sauberen Abhängigkeitsbaum überleben kann. Wenn Sie diese rm -rf-Absicherung jedoch jemals entfernen, würde ein host-gebautes vendor/ im Kontext zuerst landen, und die Kopie aus der Vendor-Stufe würde nur die Pfade überschreiben, die der saubere Baum enthält – alle zusätzlichen Host-Dateien (ein veraltetes oder dev-installiertes Paket, eine verwaiste Klasse) würden dann darunter überleben. Das Heraushalten von vendor/ aus dem Kontext schließt dieses Loch unabhängig vom 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

Schließen Sie die echten lokalen Geheimnisdateien aus (.env, .env.local, .env.*.local), nicht ein pauschales .env.* – dieser Platzhalter verwirft auch nicht-geheime Vorlagen wie .env.example, die Sie durchaus ausliefern möchten, damit das Image eine dokumentierte Konfigurationsgrundlage trägt. Behalten Sie jede eingecheckte, nicht-geheime Env-Vorlage im Kontext; schließen Sie nur die Dateien aus, die tatsächlich lokale Geheimnisse enthalten.

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

Die Abhängigkeitsstufe läuft mit --no-scripts, sodass kein Post-Install-Hook der Anwendung gegen einen unvollständigen Baum ausgeführt wird; führen Sie jeden Anwendungs-Build-Schritt (Asset-Kompilierung, Cache-Warming) in einer späteren Stufe aus, nachdem der Code kopiert wurde.

Mehrstufige Composer-Installation (keine Dev-Abhängigkeiten)

Abschnitt betitelt „Mehrstufige Composer-Installation (keine Dev-Abhängigkeiten)“

Das ausgelieferte Image darf kein Entwicklungswerkzeug enthalten. Das --no-dev-Flag bei composer install ist die tragende Zeile: Es überspringt alles unter require-dev in nextpdf/core und Ihrer Anwendung – den Test-Runner, den statischen Analyzer und die Mutationswerkzeuge –, von denen keines einen Platz in der Produktion hat. Kombinieren Sie es mit --optimize-autoloader, sodass der Autoloader eine generierte Klassenkarte ist statt eines Dateisystem-Scans bei jeder Anfrage.

Kopieren Sie composer.json und composer.lock vor dem Rest des Quellcodes, damit Docker die Abhängigkeitsschicht cacht und nur dann neu auflöst, wenn sich die Lock-Datei ändert. Da diese erste Installation gegen die Lock-Datei allein läuft – ohne den Anwendungs-Quellcode –, baut --optimize-autoloader dort die Klassenkarte nur für den Vendor-Baum; die eigenen Klassen Ihrer Anwendung sind noch nicht vorhanden. Deshalb führt die Laufzeitstufe einmal composer dump-autoload --optimize --no-dev --no-scripts aus, nachdem der Quellcode kopiert wurde: Es faltet die Klassen der App in dieselbe optimierte Klassenkarte. Führen Sie kein separates composer dump-autoload in einem Worktree aus, in dem Sie auch entwickeln (es würde eine Produktions-Klassenkarte in einen Dev-Baum einchecken); die Neugenerierung gehört in das Image, nach dem Kopieren des Quellcodes, wie oben gezeigt.

Die native Engine löst Schriften aus Schriftdateien auf, die sie lesen kann, nicht aus OS-installierten Schriften. Das Installieren von fonts-*-Paketen oder das Ausführen von fc-cache bewirkt nichts, was der native Pfad sehen kann, sodass dieses Image keine Systemschriften installiert. Bündeln Sie Ihre .ttf / .otf-Dateien unter resources/fonts/; das obige COPY . /var/www/app trägt sie bereits ins Image.

Die Dateien ins Image zu bekommen, ist nur die halbe Arbeit. Die nackte native Engine liest keine Schriftsuch-Umgebungsvariable – NEXTPDF_FONTS_PATH ist der Standardwert des fonts_path-Konfigurationsschlüssels des Pakets nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) und wird nur von dieser Framework-Integration verbraucht, nicht von nextpdf/core. Ein schlichter php bin/generate.php-Entrypoint mit nur dieser gesetzten Variablen registriert keine Schriften und rendert dasselbe Tofu, das dieses Image verhindern soll. Der Entrypoint muss das gebündelte Verzeichnis in PHP registrieren:

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();

Das ist die gesamte Docker-Angelegenheit für Schriften. Die Dateibenennungsregeln, die Registry-API, das Warmup-and-lock-Muster und die Handhabung schreibgeschützter Dateisysteme liegen allesamt auf der dedizierten Seite – duplizieren Sie sie nicht hier. Lesen Sie Schriften für die native Engine in der Produktion bereitstellen für das vollständige Muster und registrieren Sie dasselbe Verzeichnis, das Sie gebündelt haben.

Die offiziellen PHP-Images laufen standardmäßig als root. Ein PDF-Generator braucht kein Root, also erstellen Sie einen unprivilegierten Benutzer und wechseln Sie zu ihm. Das obige Dockerfile fügt einen Systembenutzer appuser mit einer festen hohen UID (10001) hinzu, gibt ihm den Besitz des Anwendungsbaums und endet mit USER appuser, sodass jeder Prozess, den der Container startet, unprivilegiert ist.

Halten Sie die Anwendung zur Laufzeit schreibgeschützt, wo Sie können. Die Engine liest ihre Schriftdateien und schreibt nur ihre Ausgabe und einen optionalen Cache geparster Schriften, sodass ein readOnlyRootFilesystem-Container funktioniert, solange der Ausgabepfad und jedes Cache-Verzeichnis beschreibbare Mounts sind. Kombinieren Sie dies mit verworfenen Linux-Capabilities und einem no-new-privileges-Flag in Ihrem Orchestrator für gestaffelte Sicherheit.

Opcache zahlt sich für langlebige PHP-Worker aus – einen FPM-Pool oder einen Apache-mod_php-Prozess, der viele Anfragen aus einem warmen Prozess bedient. Diese Prozesse kompilieren Ihre Klassen einmal und führen dann auf einem heißen Pfad nie wieder ein stat auf Quelldateien aus, was genau das ist, was Ihnen opcache.validate_timestamps=0 einbringt. Opcache ist auf dem offiziellen php:8.4-Image nicht ab Werk aktiviert, sodass das obige Dockerfile es mit docker-php-ext-install opcache installiert (Sie können äquivalent docker-php-ext-enable opcache, wenn die Erweiterung bereits kompiliert ist). Die conf.d-Datei unten ist Abstimmung, nicht der Aktivierungsschritt – sie bewirkt nichts, bis die Erweiterung geladen ist. Liefern Sie sie als conf.d-Include aus (docker/opcache.ini, im Dockerfile kopiert):

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 bedeutet, dass der Cache Quelldateien nie erneut prüft – korrekt für ein unveränderliches Image, da sich der Code nur durch ein neues Image ändert. Stimmen Sie memory_consumption und max_accelerated_files auf die Klassenanzahl Ihrer Anwendung ab.

Das gezeigte CMD ist ein einmaliger CLI-Generator, und opcache.enable_cli=0 ist dafür korrekt. Ein kurzlebiger php bin/generate.php-Prozess startet, kompiliert, rendert einmal und beendet sich, sodass ein Opcode-Cache, den er nicht mit einer nächsten Anfrage teilen kann, keinen Nutzen bringt – lassen Sie das CLI-Opcache aus und zahlen Sie keinen seiner Speicherkosten. Opcache verdient sich seinen Platz nur dort, wo der Prozess wiederverwendet wird: ein FPM/Apache-SAPI oder ein echt langlaufender CLI-Worker (ein Queue-Consumer oder ein Server im RoadRunner-Stil). Nur diese Art von residentem CLI-Worker würde opcache.enable_cli=1 setzen; für den einmaligen Generator hier belassen Sie es bei 0.

Wenn Sie ein Setup betreiben, das Opcache-Preloading verwendet (ein langlebiger FPM-Worker mit einem opcache.preload-Skript), setzen Sie opcache.preload=/path/to/preload.php und fügen opcache.preload_user=appuser hinzu, sodass das Preload als der unprivilegierte Benutzer läuft. Ohne ein tatsächliches opcache.preload-Skript bewirkt opcache.preload_user nichts, weshalb es nicht in der Baseline-Konfiguration oben steht – fügen Sie es nicht hinzu, sofern Sie nicht auch opcache.preload setzen.

Fügen Sie einen Verifizierungsschritt hinzu, sodass ein fehlgebautes Image lautstark scheitert, statt bei der ersten Anfrage Tofu oder einen Fatal zu erzeugen. NextPDF liefert eine CLI aus, deren doctor-Befehl die laufende PHP-Umgebung inspiziert und genau über die Erweiterungen berichtet, die die Engine interessieren – openssl, zlib, mbstring, gd, curl und intl. Das Paket deklariert "bin": ["bin/nextpdf"], sodass Composer in einer konsumierenden Anwendung die ausführbare Datei unter vendor/bin/nextpdf installiert (nicht bin/nextpdf, was der Pfad innerhalb des Pakets nextpdf/core selbst ist). Führen Sie sie innerhalb des gebauten Images aus:

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

Ein gesundes Ergebnis bestätigt, dass PHP 8.4 und jede erforderliche Erweiterung geladen ist. Verdrahten Sie denselben Aufruf in den Build (oder einen CI-Smoke-Job), sodass eine fehlende Erweiterung die Pipeline stoppt:

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

Rendern Sie für eine End-to-End-Prüfung eine Seite über Ihren eigenen Entrypoint und prüfen Sie die Ausgabe, wie die Schriftenseite es für eine Schrift-Smoke-Prüfung beschreibt.

  • php:8.4-fpm oder -apache statt -cli. Verwenden Sie die SAPI, unter der Ihre App tatsächlich bedient. Die Erweiterungsliste ist identisch; nur das Basis-Tag und das CMD/der Entrypoint unterscheiden sich. Für einen Queue-Worker oder einen CLI-Batch-Job ist -cli korrekt.
  • Alpine (php:8.4-alpine) benötigt andere Paketnamen. Die apt-get-Zeilen oben sind für das Debian-basierte Standard-Image. Installieren Sie auf Alpine die *-dev-Header als virtuelle Build-Gruppe (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) und führen Sie nach dem Schritt docker-php-ext-install gd intl mbstring opcache apk del .build-deps aus – aber installieren Sie zuerst mit apk add --no-cache die Laufzeit-Bibliotheken, gegen die die Erweiterungen linken (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma), sodass das Löschen der Build-Gruppe nicht intl.so / gd.so / mbstring.so aushängt. Dies ist dieselbe Behalte-die-Laufzeit-Bibliotheken-Regel, die der Debian-Block mit apt-mark durchsetzt.
  • Installieren Sie keine fonts-*-Pakete. Sie sind für die native Engine unsichtbar. Bündeln Sie stattdessen Schriftdateien – siehe die oben verlinkte Schriftenseite.
  • Premium und ionCube sind eine andere Image-Angelegenheit. Die ionCube-codierten NextPDF-Pro- / Enterprise-Builds benötigen den ionCube-Loader, der im Image installiert und auf den exakten PHP-Build des Containers abgestimmt ist (8.4, NTS vs. ZTS). Das ist für ein Core-Image außerhalb des Geltungsbereichs; wenn Sie Premium deployen, folgen Sie dem Docker-Abschnitt von ionCube-Loader-Einrichtung.
  • Halten Sie ein Host-vendor/ aus dem Build-Kontext heraus. Die .dockerignore (die vendor/, .git/ und lokale Caches ausschließt) hält den Host-Baum vollständig aus dem Kontext heraus – das ist es, was den Build klein, schnell und frei von durchgesickerten lokalen Geheimnissen macht. Sie schützt auch den Verzeichnis-Merge-Fall: Ein Verzeichnis-COPY ist ein Merge, kein Ersetzen, sodass ein host-gebautes vendor/, das den Kontext erreicht hätte, zuerst landen würde und das COPY --from=vendor /app/vendor /var/www/app/vendor nur die Pfade überschreiben würde, die der saubere Abhängigkeitsbaum enthält. In diesem Dockerfile entfernt das RUN rm -rf /var/www/app/vendor vor der Vendor-Kopie bereits jedes solche Verzeichnis, sodass dieser Rückstand hier nicht auftreten kann; das Merge-Risiko kehrt nur zurück, wenn Sie diese rm -rf-Absicherung weglassen, weshalb der .dockerignore-Ausschluss die dauerhafte Lösung ist.
  • Liefern Sie keine Dev-Abhängigkeiten aus. --no-dev hält die Test- und Analysewerkzeuge sowie ihre transitiven Pakete aus dem Laufzeit-Image und seiner Angriffsfläche heraus.
  • Laufen Sie unprivilegiert. Das abschließende USER appuser stellt sicher, dass kein Container-Prozess als Root läuft. Kombinieren Sie es mit einem schreibgeschützten Root-Dateisystem und verworfenen Capabilities in Ihrem Orchestrator.
  • Pinnen Sie das Basis-Image. Pinnen Sie php:8.4 in der Produktion auf einen Digest, sodass ein Rebuild nicht stillschweigend eine veränderte Basis ziehen kann, und bauen Sie in einem Rhythmus neu, um Sicherheitspatches bewusst aufzunehmen.
  • Halten Sie Schriften und Lizenzen aus öffentlichen Schichten heraus. Bündeln Sie nur Schriften, die Sie einbetten dürfen, und backen Sie niemals eine Premium-Lizenzdatei in ein öffentlich gepushtes Image ein – mounten Sie sie stattdessen zur Laufzeit.

Diese Anleitung erhebt keinen normativen Standardanspruch. Die Plattformfakten werden direkt aus dem Paket nextpdf/core gelesen: der php: >=8.4 <9.0-Constraint und die erforderlichen Erweiterungen ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib und ext-curl. Der Verifizierungsbefehl ist der echte nextpdf-CLI-doctor-Handler – deklariert als "bin": ["bin/nextpdf"] in nextpdf/core und daher unter vendor/bin/nextpdf in einer konsumierenden App installiert –, der über dieselbe Erweiterungsmenge berichtet. Die native Engine registriert Schriften über NextPDF\Typography\FontRegistry (das Verzeichnis-Konstruktorargument / addFontDirectory()), verdrahtet über NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH ist der fonts_path-Konfigurationsschlüssel des Pakets nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), keine Variable, die nextpdf/core liest. Das Registry-Verhalten ist auf der unter „Siehe auch“ verlinkten Schriftenseite dokumentiert.