Ga naar inhoud
getnextpdf.com

Een NextPDF-applicatie containeriseren

Je wilt een kleine, reproduceerbare Docker-image die de native, in-process NextPDF core-engine draait — composer require nextpdf/core, die PDF’s genereert binnen je PHP-proces. Deze pagina bouwt precies dat: een php:8.4-image met alleen de extensies die de engine daadwerkelijk nodig heeft, geen ontwikkelingsafhankelijkheden in de laatste laag, meegeleverde lettertypen, een non-root-runtime-gebruiker, opcache afgestemd op productie, en een verificatiestap die de build laat falen als er iets ontbreekt.

Deze pagina is alleen voor de native engine. De Chrome-brug (writeHtmlChrome via nextpdf/artisan) en de Connect-server zijn aparte runtimes met hun eigen, zwaardere images — een headless-Chromium-installatie voor de brug, een langlevende service voor Connect. Voeg geen browser of server toe aan deze image; de native engine heeft geen van beide nodig.

Bevestig voordat je begint dat deze onderdelen op hun plek staan:

  • Je applicatie heeft een gecommitte composer.json en composer.lock, met nextpdf/core als afhankelijkheid.
  • Je hebt de lettertypebestanden die je wilt insluiten, en je hebt het recht ze in te sluiten.
  • Je kunt docker build uitvoeren tegen je applicatiemap.

Dit is een operationele how-to. Er is hier vrijwel geen PHP; het werk is de Dockerfile en een paar omgevingsinstellingen.

De image moet voldoen aan de echte platformbeperkingen van de engine, niet meer. Rechtstreeks uit het pakket gelezen vereist nextpdf/core php: >=8.4 <9.0 en deze PHP-extensies:

ExtensieWaarom de engine die nodig heeft
ext-mbstringMulti-byte string-afhandeling voor tekst en coderingen
ext-intlUnicode-, locale- en internationaliseringsondersteuning
ext-gdDecodering en verwerking van rasterafbeeldingen
ext-opensslCryptografie voor ondertekenen en veilig hashen
ext-zlibStream-compressie (Flate) van PDF-objecten
ext-curlHTTP-client voor de uitgaande aanroepen van de engine

Breng die in kaart op de officiële php:8.4-image. openssl, curl en zlib zijn al ingecompileerd in de officiële PHP-image, dus die hoef je niet te docker-php-ext-install-en. mbstring, gd en intl zijn niet meegeleverd en moeten worden geïnstalleerd, en elk heeft eerst zijn systeem-development-headers nodig — mbstring heeft daarnaast de build-afhankelijkheid libonig-dev (Oniguruma) nodig. Voeg geen engine-extensies toe die het pakket niet vermeldt — elke extra docker-php-ext-install is buildtijd en aanvalsoppervlak dat je niet nodig hebt. De enige niet-engine-extensie die deze image wel installeert is opcache: het is een runtime-prestatie-extensie, niet standaard ingeschakeld op de officiële image, en de opcache-afstemming hieronder hangt af van zijn aanwezigheid (zie “Opcache voor productie”).

Dit is een two-stage-build. De eerste stage installeert Composer-afhankelijkheden met de ontwikkelingspakketten uitgesloten; de tweede stage is de slanke runtime-image die wordt verscheept.

Voeg eerst een .dockerignore toe naast de Dockerfile. De primaire taak ervan is de host-omgeving — een op de host gebouwde vendor/, lokale secret-bestanden en build-caches — volledig buiten de buildcontext te houden, zodat COPY . /var/www/app alleen verscheept wat je bedoelt: kleinere, snellere en veiligere builds die geen lokale .env-secrets kunnen lekken of megabytes host-vendor/ in de image kunnen dragen.

Het uitsluiten van vendor/ is ook van belang omdat een map-COPY een merge is, geen replace. De Dockerfile hieronder voert RUN rm -rf /var/www/app/vendor uit vóór de COPY --from=vendor ... /var/www/app/vendor, dus in deze image kan een host-vendor/ nooit overleven onder de schone afhankelijkheidsboom. Maar als je die rm -rf-beveiliging ooit verwijdert, zou een op de host gebouwde vendor/ in de context als eerste landen en zou de vendor-stage-kopie alleen de paden overschrijven die de schone boom bevat — eventuele extra host-bestanden (een verouderd of dev-geïnstalleerd pakket, een verweesde klasse) zouden er dan onderdoor overleven. vendor/ buiten de context houden sluit dat gat ongeacht de 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

Sluit de echte lokale secret-bestanden uit (.env, .env.local, .env.*.local), geen pauschale .env.* — die wildcard verwijdert ook niet-secret-templates zoals .env.example die je wel wilt verschepen zodat de image een gedocumenteerde configuratiebaseline draagt. Houd elke gecommitte, niet-secret env-template in de context; sluit alleen de bestanden uit die daadwerkelijk lokale secrets bevatten.

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

De afhankelijkheidsstage draait met --no-scripts zodat er geen applicatie-post-install-hook draait tegen een onvolledige boom; voer een applicatie-buildstap (asset-compilatie, cache-warming) uit in een latere stage nadat de code is gekopieerd.

Multi-stage Composer-install (geen dev-afhankelijkheden)

Sectie met titel “Multi-stage Composer-install (geen dev-afhankelijkheden)”

De verscheepte image mag geen ontwikkelingstooling bevatten. De --no-dev-vlag op composer install is de dragende regel: het slaat alles onder require-dev in nextpdf/core en je applicatie over — de test runner, statische analyzer en mutatietools — die geen enkele plaats hebben in productie. Combineer het met --optimize-autoloader zodat de autoloader een gegenereerde class map is in plaats van een filesystem-scan bij elke request.

Kopieer composer.json en composer.lock vóór de rest van de bron zodat Docker de afhankelijkheidslaag cachet en alleen opnieuw oplost wanneer het lock-bestand verandert. Omdat die eerste install draait tegen het lock-bestand alleen — zonder de applicatiebron — bouwt --optimize-autoloader daar de class map alleen voor de vendor-boom; de eigen klassen van je applicatie zijn er nog niet. Daarom draait de runtime-stage eenmaal composer dump-autoload --optimize --no-dev --no-scripts nadat de bron is gekopieerd: het vouwt de klassen van de app in dezelfde geoptimaliseerde class map. Draai geen aparte composer dump-autoload in een worktree waarin je ook ontwikkelt (dat zou een productie-class-map in een dev-boom committen); de herbouw hoort in de image, na het kopiëren van de bron, zoals hierboven getoond.

De native engine lost lettertypen op uit lettertypebestanden die het kan lezen, niet uit OS-geïnstalleerde lettertypen. fonts-*-pakketten installeren of fc-cache draaien doet niets wat het native pad kan zien, dus deze image installeert geen systeemlettertypen. Lever je .ttf- / .otf-bestanden mee onder resources/fonts/; de COPY . /var/www/app hierboven draagt ze al in de image.

De bestanden in de image krijgen is slechts de helft van het werk. De kale native engine leest geen lettertype-zoek-omgevingsvariabele — NEXTPDF_FONTS_PATH is de standaardwaarde van de fonts_path-configuratiekey van het nextpdf/laravel-pakket (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) en wordt alleen door die framework-integratie verbruikt, niet door nextpdf/core. Een gewone php bin/generate.php-entrypoint met alleen die variabele ingesteld registreert geen lettertypen en rendert dezelfde tofu die deze image moet voorkomen. De entrypoint moet de meegeleverde map in PHP registreren:

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

Dat is de hele Docker-zorg voor lettertypen. De bestandsnaamregels, de register-API, het warmup-and-lock-patroon en de read-only-filesystem-afhandeling leven allemaal op de speciale pagina — dupliceer ze hier niet. Lees Lettertypen voorzien voor de native engine in productie voor het volledige patroon, en registreer dezelfde map die je hebt meegeleverd.

De officiële PHP-images draaien standaard als root. Een PDF-generator heeft geen root nodig, dus maak een onbevoorrechte gebruiker en schakel daarnaar over. De Dockerfile hierboven voegt een systeemgebruiker appuser toe met een vaste hoge UID (10001), geeft die eigenaarschap over de applicatieboom, en eindigt met USER appuser zodat elk proces dat de container start onbevoorrecht is.

Houd de applicatie waar mogelijk read-only tijdens runtime. De engine leest zijn lettertypebestanden en schrijft alleen zijn uitvoer en een optionele cache met geparste lettertypen, dus een readOnlyRootFilesystem-container werkt zolang het uitvoerpad en eventuele cache-map schrijfbare mounts zijn. Combineer dit met gedropte Linux-capabilities en een no-new-privileges-vlag in je orchestrator voor diepteverdediging.

Opcache loont voor langlevende PHP-workers — een FPM-pool of een Apache mod_php-proces dat veel requests bedient vanuit één warm proces. Die processen compileren je klassen eenmaal en stat’en daarna nooit bronbestanden op een hot path, wat precies is wat opcache.validate_timestamps=0 je oplevert. Opcache is niet standaard ingeschakeld op de officiële php:8.4-image, dus de Dockerfile hierboven installeert het met docker-php-ext-install opcache (je kunt gelijkwaardig docker-php-ext-enable opcache als de extensie al is gecompileerd). Het conf.d-bestand hieronder is afstemming, niet de inschakelstap — het doet niets totdat de extensie is geladen. Verscheep het als een conf.d-include (docker/opcache.ini, gekopieerd in de 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 betekent dat de cache nooit bronbestanden hercontroleert — correct voor een immutable image, omdat de enige manier waarop de code verandert een nieuwe image is. Stem memory_consumption en max_accelerated_files af op het klassenaantal van je applicatie.

De getoonde CMD is een one-shot CLI-generator, en opcache.enable_cli=0 is daar correct voor. Een kortlevend php bin/generate.php-proces start, compileert, rendert eenmaal en stopt, dus een opcode-cache die het niet met een volgende request kan delen, levert geen voordeel — laat CLI-opcache uit en betaal niets van zijn geheugenkost. Opcache verdient zijn loon alleen waar het proces wordt hergebruikt: een FPM/Apache-SAPI, of een werkelijk langlevende CLI-worker (een queue consumer of een RoadRunner-achtige server). Alleen dat soort residente CLI-worker zou opcache.enable_cli=1 instellen; voor de one-shot-generator hier houd je het 0.

Als je wel een opzet draait die opcache-preloading gebruikt (een langlevende FPM-worker met een opcache.preload-script), stel dan opcache.preload=/path/to/preload.php in en voeg opcache.preload_user=appuser toe zodat de preload draait als de onbevoorrechte gebruiker. Zonder een werkelijk opcache.preload-script doet opcache.preload_user niets, en daarom staat het niet in de baseline-config hierboven — voeg het niet toe tenzij je ook opcache.preload instelt.

Voeg een verificatiestap toe zodat een verkeerd gebouwde image luid faalt in plaats van tofu of een fatal bij de eerste request te produceren. NextPDF levert een CLI mee waarvan het doctor-commando de draaiende PHP-omgeving inspecteert en rapporteert over precies de extensies waar de engine om geeft — openssl, zlib, mbstring, gd, curl en intl. Het pakket declareert "bin": ["bin/nextpdf"], dus in een verbruikende applicatie installeert Composer het executable op vendor/bin/nextpdf (niet bin/nextpdf, wat het pad binnen het nextpdf/core-pakket zelf is). Voer het uit binnen de gebouwde image:

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

Een gezond resultaat bevestigt PHP 8.4 en dat elke vereiste extensie is geladen. Bedraad dezelfde aanroep in de build (of een CI-smoke-job) zodat een ontbrekende extensie de pipeline stopt:

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

Render voor een end-to-end-check één pagina via je eigen entrypoint en asserteer op de uitvoer, zoals de lettertypenpagina beschrijft voor een lettertype-smoke-check.

  • php:8.4-fpm of -apache in plaats van -cli. Gebruik de SAPI waaronder je app daadwerkelijk bediend wordt. De extensielijst is identiek; alleen de base-tag en de CMD/entrypoint verschillen. Voor een queue worker of een CLI-batchjob is -cli correct.
  • Alpine (php:8.4-alpine) heeft andere pakketnamen nodig. De apt-get-regels hierboven zijn voor de Debian-gebaseerde standaardimage. Installeer op Alpine de *-dev-headers als een virtuele buildgroep (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev) en, na de docker-php-ext-install gd intl mbstring opcache-stap, apk del .build-deps — maar eerst apk add --no-cache de runtime-bibliotheken waartegen de extensies linken (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma) zodat het verwijderen van de buildgroep intl.so / gd.so / mbstring.so niet ontlinkt. Dit is dezelfde houd-de-runtime-libs-regel die het Debian-blok met apt-mark afdwingt.
  • Installeer geen fonts-*-pakketten. Ze zijn onzichtbaar voor de native engine. Lever in plaats daarvan lettertypebestanden mee — zie de hierboven gelinkte lettertypenpagina.
  • Premium en ionCube zijn een andere image-zorg. De ionCube-gecodeerde NextPDF Pro- / Enterprise-builds hebben de ionCube Loader nodig, geïnstalleerd in de image en gematcht aan de exacte PHP-build van de container (8.4, NTS vs. ZTS). Dat valt buiten de scope voor een core-image; volg als je premium deployt de Docker-sectie van ionCube Loader instellen.
  • Houd een host-vendor/ buiten de buildcontext. De .dockerignore (die vendor/, .git/ en lokale caches uitsluit) houdt de host-boom volledig buiten de context — dat is wat de build klein, snel en vrij van gelekte lokale secrets maakt. Het beveiligt ook tegen het map-merge-geval: een map-COPY is een merge, geen replace, dus een op de host gebouwde vendor/ die de context bereikte, zou als eerste landen en de COPY --from=vendor /app/vendor /var/www/app/vendor zou alleen de paden overschrijven die de schone afhankelijkheidsboom bevat. In deze Dockerfile verwijdert de RUN rm -rf /var/www/app/vendor vóór de vendor-kopie al elke dergelijke map, dus dat residu kan hier niet optreden; het merge-risico keert alleen terug als je die rm -rf-beveiliging laat vallen, en daarom is de .dockerignore-uitsluiting de duurzame fix.
  • Verscheep geen dev-afhankelijkheden. --no-dev houdt de test- en analysetooling, en hun transitieve pakketten, buiten de runtime-image en zijn aanvalsoppervlak.
  • Draai onbevoorrecht. De laatste USER appuser zorgt dat geen containerproces als root draait. Combineer het met een read-only root-filesystem en gedropte capabilities in je orchestrator.
  • Pin de base-image. Pin php:8.4 aan een digest in productie zodat een herbouw niet stilzwijgend een gewijzigde base kan binnenhalen, en herbouw op een cadans om beveiligingspatches bewust op te pikken.
  • Houd lettertypen en licenties buiten publieke lagen. Lever alleen lettertypen mee die je het recht hebt in te sluiten, en bak nooit een premium-licentiebestand in een publiek gepushte image — mount het in plaats daarvan tijdens runtime.

Deze gids doet geen normatieve standaardenclaim. De platformfeiten worden rechtstreeks uit het nextpdf/core-pakket gelezen: de php: >=8.4 <9.0-constraint en de vereiste extensies ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib en ext-curl. Het verificatiecommando is de echte nextpdf-CLI-doctor- handler — gedeclareerd als "bin": ["bin/nextpdf"] in nextpdf/core en daarom geïnstalleerd op vendor/bin/nextpdf in een verbruikende app — die rapporteert over dezelfde extensieset. De native engine registreert lettertypen via NextPDF\Typography\FontRegistry (het map-constructorargument / addFontDirectory()) bedraad via NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH is de fonts_path-configuratiekey van het nextpdf/laravel-pakket (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), geen variabele die nextpdf/core leest. Het registergedrag is gedocumenteerd op de lettertypenpagina die onder Zie ook is gelinkt.