Przejdź do głównej zawartości
getnextpdf.com

Konteneryzacja aplikacji NextPDF

Chcesz małego, odtwarzalnego obrazu Docker, który uruchamia natywny silnik rdzenia NextPDF działający w ramach procesucomposer require nextpdf/core, generujący pliki PDF wewnątrz Twojego procesu PHP. Ta strona buduje dokładnie to: obraz php:8.4 tylko z rozszerzeniami, których silnik faktycznie potrzebuje, bez zależności deweloperskich w warstwie końcowej, z dołączonymi czcionkami, użytkownikiem uruchomieniowym non-root, opcache dostrojonym pod produkcję oraz krokiem weryfikacji, który obala kompilację, jeśli czegoś brakuje.

Ta strona jest wyłącznie dla natywnego silnika. Mostek Chrome (writeHtmlChrome przez nextpdf/artisan) oraz serwer Connect to osobne środowiska uruchomieniowe z własnymi, cięższymi obrazami — instalacja headless Chromium dla mostka, długo działająca usługa dla Connect. Nie dodawaj przeglądarki ani serwera do tego obrazu; natywny silnik nie potrzebuje żadnego z nich.

Zanim zaczniesz, potwierdź, że te elementy są na miejscu:

  • Twoja aplikacja ma zatwierdzone composer.json i composer.lock, z nextpdf/core jako zależnością.
  • Masz pliki czcionek, które zamierzasz osadzić, i masz prawo je osadzić.
  • Możesz uruchomić docker build względem katalogu swojej aplikacji.

To poradnik operacyjny. PHP jest tu prawie nieobecne; praca tkwi w Dockerfile i kilku ustawieniach środowiskowych.

Obraz musi spełnić rzeczywiste ograniczenia platformowe silnika, nic więcej. Odczytując je wprost z pakietu, nextpdf/core wymaga php: >=8.4 <9.0 oraz tych rozszerzeń PHP:

RozszerzenieDlaczego silnik go potrzebuje
ext-mbstringObsługa łańcuchów wielobajtowych dla tekstu i kodowań
ext-intlWsparcie Unicode, ustawień regionalnych i internacjonalizacji
ext-gdDekodowanie i przetwarzanie obrazów rastrowych
ext-opensslKryptografia do podpisywania i bezpiecznego haszowania
ext-zlibKompresja strumieni (Flate) obiektów PDF
ext-curlKlient HTTP do wychodzących wywołań silnika

Odwzoruj je na oficjalny obraz php:8.4. openssl, curl oraz zlib są już wkompilowane w oficjalny obraz PHP, więc nie uruchamiasz dla nich docker-php-ext-install. mbstring, gd oraz intl nie są dołączone i muszą zostać zainstalowane, a każde z nich potrzebuje najpierw obecnych systemowych nagłówków deweloperskich — mbstring dodatkowo potrzebuje zależności kompilacyjnej libonig-dev (Oniguruma). Nie dodawaj rozszerzeń silnika, których pakiet nie wymienia — każde dodatkowe docker-php-ext-install to czas kompilacji i powierzchnia ataku, których nie potrzebujesz. Jedynym rozszerzeniem spoza silnika, które ten obraz instaluje, jest opcache: to rozszerzenie wydajności w czasie działania, niewłączone domyślnie na oficjalnym obrazie, a poniższe dostrojenie opcache zależy od jego obecności (zobacz „Opcache pod produkcję”).

To kompilacja dwuetapowa. Pierwszy etap instaluje zależności Composera z wyłączonymi pakietami deweloperskimi; drugi etap to odchudzony obraz uruchomieniowy, który jest wysyłany.

Najpierw dodaj .dockerignore obok Dockerfile. Jego głównym zadaniem jest całkowite trzymanie środowiska hosta — zbudowanego na hoście vendor/, lokalnych plików sekretów i pamięci podręcznych kompilacji — poza kontekstem kompilacji, aby COPY . /var/www/app wysyłał tylko to, co zamierzasz: mniejsze, szybsze i bezpieczniejsze kompilacje, które nie mogą wyciec lokalnych sekretów .env ani przenieść megabajtów hostowego vendor/ do obrazu.

Wykluczenie vendor/ ma znaczenie także dlatego, że COPY katalogu to scalenie, a nie zastąpienie. Poniższy Dockerfile uruchamia RUN rm -rf /var/www/app/vendor przed COPY --from=vendor ... /var/www/app/vendor, więc w tym obrazie hostowy vendor/ nigdy nie może przetrwać pod czystym drzewem zależności. Jeśli jednak kiedykolwiek usuniesz to zabezpieczenie rm -rf, zbudowany na hoście vendor/ w kontekście trafiłby tam jako pierwszy, a kopia z etapu vendor nadpisałaby tylko te ścieżki, które zawiera czyste drzewo — wszelkie dodatkowe pliki hosta (nieaktualny lub deweloperski pakiet, osierocona klasa) przetrwałyby wtedy pod spodem. Trzymanie vendor/ poza kontekstem zamyka tę lukę niezależnie od 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

Wyklucz rzeczywiste lokalne pliki sekretów (.env, .env.local, .env.*.local), a nie ryczałtowe .env.* — ten symbol wieloznaczny porzuca również niesekretne szablony, takie jak .env.example, które chcesz wysłać, aby obraz niósł udokumentowaną bazę konfiguracji. Trzymaj w kontekście dowolny zatwierdzony, niesekretny szablon env; wyklucz wyłącznie pliki, które faktycznie przechowują lokalne sekrety.

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

Etap zależności działa z --no-scripts, więc żaden poinstalacyjny hak aplikacji nie uruchamia się względem niekompletnego drzewa; uruchom dowolny krok kompilacji aplikacji (kompilację zasobów, rozgrzanie pamięci podręcznej) na późniejszym etapie, po skopiowaniu kodu.

Wieloetapowa instalacja Composera (bez zależności deweloperskich)

Dział zatytułowany „Wieloetapowa instalacja Composera (bez zależności deweloperskich)”

Wysyłany obraz nie może zawierać narzędzi deweloperskich. Flaga --no-dev w composer install to linia nośna: pomija wszystko z require-dev w nextpdf/core i Twojej aplikacji — runner testów, analizator statyczny i narzędzia do mutacji — z których żadne nie ma miejsca na produkcji. Połącz ją z --optimize-autoloader, aby autoloader był wygenerowaną mapą klas, a nie skanem systemu plików przy każdym żądaniu.

Skopiuj composer.json oraz composer.lock przed resztą źródła, aby Docker buforował warstwę zależności i rozwiązywał je ponownie tylko wtedy, gdy plik lock się zmieni. Ponieważ ta pierwsza instalacja działa wyłącznie względem pliku lock — bez źródła aplikacji — --optimize-autoloader buduje tam mapę klas tylko dla drzewa vendor; własnych klas Twojej aplikacji jeszcze nie ma. Dlatego etap uruchomieniowy uruchamia composer dump-autoload --optimize --no-dev --no-scripts raz po skopiowaniu źródła: składa klasy aplikacji do tej samej zoptymalizowanej mapy klas. Nie uruchamiaj osobnego composer dump-autoload w drzewie roboczym, w którym też programujesz (zatwierdziłoby to produkcyjną mapę klas do drzewa deweloperskiego); przebudowa należy do obrazu, po skopiowaniu źródła, jak pokazano powyżej.

Natywny silnik rozwiązuje czcionki na podstawie plików czcionek, które potrafi odczytać, a nie na podstawie czcionek zainstalowanych w systemie operacyjnym. Zainstalowanie pakietów fonts-* lub uruchomienie fc-cache nie daje nic, co mogłaby zobaczyć ścieżka natywna, więc ten obraz nie instaluje żadnych czcionek systemowych. Dołącz swoje pliki .ttf / .otf pod resources/fonts/; powyższe COPY . /var/www/app już przenosi je do obrazu.

Wprowadzenie plików do obrazu to dopiero połowa pracy. Goły natywny silnik nie odczytuje żadnej zmiennej środowiskowej wyszukiwania czcionek — NEXTPDF_FONTS_PATH to domyślna wartość klucza konfiguracji fonts_path pakietu nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))) i jest konsumowana wyłącznie przez tę integrację frameworka, a nie przez nextpdf/core. Zwykły punkt wejścia php bin/generate.php z ustawioną jedynie tą zmienną nie rejestruje żadnych czcionek i renderuje to samo tofu, któremu ten obraz ma zapobiegać. Punkt wejścia musi zarejestrować dołączony katalog w 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();

To cała dockerowa troska o czcionki. Reguły nazewnictwa plików, API rejestru, wzorzec rozgrzewki-i-zablokowania oraz obsługa systemu plików tylko do odczytu — wszystko żyje na dedykowanej stronie; nie duplikuj tego tutaj. Przeczytaj Udostępnianie czcionek dla natywnego silnika w środowisku produkcyjnym, aby poznać pełny wzorzec, i zarejestruj ten sam katalog, który dołączyłeś.

Oficjalne obrazy PHP domyślnie działają jako root. Generator PDF nie potrzebuje roota, więc utwórz użytkownika nieuprzywilejowanego i przełącz się na niego. Powyższy Dockerfile dodaje użytkownika systemowego appuser ze stałym wysokim UID (10001), nadaje mu własność drzewa aplikacji i kończy się USER appuser, aby każdy proces uruchamiany przez kontener był nieuprzywilejowany.

Utrzymuj aplikację tylko do odczytu w czasie działania, gdzie tylko możesz. Silnik odczytuje swoje pliki czcionek, a zapisuje jedynie swoje wyjście i opcjonalną pamięć podręczną sparsowanych czcionek, więc kontener readOnlyRootFilesystem działa, dopóki ścieżka wyjściowa i dowolny katalog pamięci podręcznej są zapisywalnymi montowaniami. Połącz to z odjętymi zdolnościami Linuksa oraz flagą no-new-privileges w Twoim orkiestratorze, aby uzyskać obronę w głąb.

Opcache opłaca się dla długowiecznych procesów roboczych PHP — puli FPM lub procesu mod_php Apache, który obsługuje wiele żądań z jednego rozgrzanego procesu. Te procesy kompilują Twoje klasy raz i potem nigdy nie sprawdzają plików źródłowych na gorącej ścieżce, czyli dokładnie to, co kupuje Ci opcache.validate_timestamps=0. Opcache nie jest włączony od ręki na oficjalnym obrazie php:8.4, więc powyższy Dockerfile instaluje go przez docker-php-ext-install opcache (możesz równoważnie użyć docker-php-ext-enable opcache, jeśli rozszerzenie jest już skompilowane). Poniższy plik conf.d to dostrojenie, a nie krok włączenia — nie robi nic, dopóki rozszerzenie nie zostanie załadowane. Wyślij go jako dołączenie conf.d (docker/opcache.ini, skopiowane w 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 oznacza, że pamięć podręczna nigdy ponownie nie sprawdza plików źródłowych — poprawne dla niezmiennego obrazu, ponieważ jedynym sposobem zmiany kodu jest nowy obraz. Dostrój memory_consumption oraz max_accelerated_files do liczby klas Twojej aplikacji.

Pokazane CMD to jednorazowy generator CLI, a opcache.enable_cli=0 jest dla niego poprawne. Krótkowieczny proces php bin/generate.php startuje, kompiluje, renderuje raz i kończy, więc pamięć podręczna opkodów, której nie może współdzielić z następnym żądaniem, nie daje korzyści — pozostaw opcache CLI wyłączony i nie płać żadnego z jego kosztów pamięci. Opcache zarabia na siebie tam, gdzie proces jest wielokrotnie używany: SAPI FPM/Apache albo genuinie długo działający proces roboczy CLI (konsument kolejki lub serwer w stylu RoadRunner). Tylko taki rezydentny proces roboczy CLI ustawiłby opcache.enable_cli=1; dla jednorazowego generatora tutaj zachowaj 0.

Jeśli prowadzisz konfigurację korzystającą z wstępnego ładowania (preloading) opcache (długowieczny proces roboczy FPM ze skryptem opcache.preload), ustaw opcache.preload=/path/to/preload.php i dodaj opcache.preload_user=appuser, aby wstępne ładowanie działało jako użytkownik nieuprzywilejowany. Bez rzeczywistego skryptu opcache.preload opcache.preload_user nic nie robi, dlatego nie ma go w powyższej bazowej konfiguracji — nie dodawaj go, chyba że ustawisz też opcache.preload.

Dodaj krok weryfikacji, aby źle zbudowany obraz zawiódł głośno, zamiast wytwarzać tofu lub błąd krytyczny przy pierwszym żądaniu. NextPDF dostarcza CLI, którego polecenie doctor inspekcjonuje działające środowisko PHP i raportuje dokładnie te rozszerzenia, na których silnikowi zależy — openssl, zlib, mbstring, gd, curl oraz intl. Pakiet deklaruje "bin": ["bin/nextpdf"], więc w aplikacji konsumującej Composer instaluje plik wykonywalny w vendor/bin/nextpdf (a nie bin/nextpdf, czyli ścieżce wewnątrz samego pakietu nextpdf/core). Uruchom go wewnątrz zbudowanego obrazu:

Okno terminala
docker run --rm your-app:latest php vendor/bin/nextpdf doctor

Zdrowy wynik potwierdza, że załadowane są PHP 8.4 i każde wymagane rozszerzenie. Wpnij to samo wywołanie w kompilację (lub zadanie dymne CI), aby brakujące rozszerzenie zatrzymało potok:

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

Dla kontroli kompleksowej (end-to-end) wyrenderuj jedną stronę przez swój własny punkt wejścia i sprawdź wyjście, tak jak opisuje strona o czcionkach dla kontroli dymnej czcionek.

  • php:8.4-fpm lub -apache zamiast -cli. Użyj SAPI, pod którym Twoja aplikacja faktycznie jest obsługiwana. Lista rozszerzeń jest identyczna; różni się tylko tag bazowy oraz CMD/punkt wejścia. Dla procesu roboczego kolejki lub wsadowego zadania CLI poprawne jest -cli.
  • Alpine (php:8.4-alpine) potrzebuje innych nazw pakietów. Powyższe linie apt-get są dla domyślnego obrazu opartego na Debianie. Na Alpine zainstaluj nagłówki *-dev jako wirtualną grupę kompilacyjną (apk add --no-cache --virtual .build-deps icu-dev libpng-dev freetype-dev libjpeg-turbo-dev oniguruma-dev), a po kroku docker-php-ext-install gd intl mbstring opcache użyj apk del .build-deps — ale najpierw apk add --no-cache biblioteki uruchomieniowe, z którymi linkują się rozszerzenia (icu-libs, libpng, freetype, libjpeg-turbo, oniguruma), aby usunięcie grupy kompilacyjnej nie odlinkowało intl.so / gd.so / mbstring.so. To ta sama reguła zachowania bibliotek uruchomieniowych, którą blok Debiana wymusza przez apt-mark.
  • Nie instaluj pakietów fonts-*. Są niewidoczne dla natywnego silnika. Zamiast tego dołącz pliki czcionek — zobacz stronę o czcionkach, do której odnosi się powyższy odnośnik.
  • Premium i ionCube to osobna troska o obraz. Zakodowane przez ionCube kompilacje NextPDF Pro / Enterprise potrzebują Loadera ionCube zainstalowanego w obrazie i dopasowanego do dokładnej kompilacji PHP kontenera (8.4, NTS kontra ZTS). To wykracza poza zakres obrazu rdzenia; jeśli wdrażasz premium, wykonaj kroki z sekcji Docker w Konfiguracja Loadera ionCube.
  • Trzymaj hostowy vendor/ poza kontekstem kompilacji. Plik .dockerignore (wykluczający vendor/, .git/ i lokalne pamięci podręczne) całkowicie trzyma drzewo hosta poza kontekstem — to właśnie czyni kompilację małą, szybką i wolną od wycieków lokalnych sekretów. Zabezpiecza też przypadek scalania katalogów: COPY katalogu to scalenie, a nie zastąpienie, więc zbudowany na hoście vendor/, który dotarł do kontekstu, trafiłby tam jako pierwszy, a COPY --from=vendor /app/vendor /var/www/app/vendor nadpisałby tylko te ścieżki, które zawiera czyste drzewo zależności. W tym Dockerfile RUN rm -rf /var/www/app/vendor przed kopią vendor już usuwa dowolny taki katalog, więc ta pozostałość nie może tu wystąpić; ryzyko scalenia wraca tylko wtedy, gdy porzucisz to zabezpieczenie rm -rf, dlatego wykluczenie w .dockerignore jest trwałą poprawką.
  • Nie wysyłaj zależności deweloperskich. --no-dev utrzymuje narzędzia testowe i analizy, a także ich tranzytywne pakiety, poza obrazem uruchomieniowym i jego powierzchnią ataku.
  • Uruchamiaj nieuprzywilejowanie. Końcowe USER appuser zapewnia, że żaden proces kontenera nie działa jako root. Połącz to z systemem plików tylko do odczytu oraz odjętymi zdolnościami w Twoim orkiestratorze.
  • Przypnij obraz bazowy. Przypnij php:8.4 do skrótu (digest) na produkcji, aby przebudowa nie mogła po cichu pobrać zmienionej bazy, i przebudowuj cyklicznie, aby świadomie wprowadzać poprawki bezpieczeństwa.
  • Trzymaj czcionki i licencje poza publicznymi warstwami. Dołączaj wyłącznie czcionki, które masz prawo osadzić, i nigdy nie wpiekaj pliku licencji premium w publicznie wypchnięty obraz — zamiast tego zamontuj go w czasie działania.

Ten przewodnik nie zgłasza normatywnego roszczenia co do standardów. Fakty platformowe są odczytane wprost z pakietu nextpdf/core: ograniczenie php: >=8.4 <9.0 oraz wymagane rozszerzenia ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib i ext-curl. Polecenie weryfikacji to prawdziwy handler doctor CLI nextpdf — zadeklarowany jako "bin": ["bin/nextpdf"] w nextpdf/core, a więc instalowany w vendor/bin/nextpdf w aplikacji konsumującej — który raportuje ten sam zestaw rozszerzeń. Natywny silnik rejestruje czcionki przez NextPDF\Typography\FontRegistry (argument konstruktora katalogu / addFontDirectory()) wpięty przez NextPDF\Core\DocumentFactory; NEXTPDF_FONTS_PATH to klucz konfiguracji fonts_path pakietu nextpdf/laravel (env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), a nie zmienna, którą odczytuje nextpdf/core. Zachowanie rejestru jest udokumentowane na stronie o czcionkach, do której odnosi się sekcja Zobacz także.