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

Uruchamianie NextPDF na platformach serverless

Natywny, działający w procesie rdzeniowy silnik NextPDF to niemal idealne obciążenie serverless. To czysty PHP działający wewnątrz twojego procesucomposer require nextpdf/core, zbuduj dokument, odbierz bajty. Nie ma zewnętrznego pliku binarnego do uruchomienia, nie ma przeglądarki headless, nie ma demona do utrzymywania przy życiu ani gniazda do usługi sidecar. Funkcja, która buduje PDF, startuje na zimno, uruchamia twój PHP, zwraca bajty i kończy działanie. To czysto mapuje się na AWS Lambda (przez środowisko Bref), Google Cloud Run oraz AWS App Runner.

Ta strona opisuje wdrożenie tego natywnego silnika do tych trzech środowisk oraz niewielki zestaw rzeczywistych ograniczeń, które one narzucają:

  • system plików środowiska uruchomieniowego jest nietrwały: Lambda gwarantuje jedynie zapisywalny /tmp, podczas gdy środowiska kontenerowe (Cloud Run, App Runner) mają ulotny, ograniczony do kontenera system plików — tak czy inaczej czcionki muszą podróżować wewnątrz paczki wdrożeniowej lub obrazu i być zarejestrowane w PHP (silnik nie odczytuje żadnej zmiennej środowiskowej ze ścieżką czcionek);
  • zimne starty płacą za autoloading i ewentualne rozgrzewanie czcionek, więc rozgrzej FontRegistry raz na kontener, a nie raz na wywołanie;
  • rozmiar paczki, pamięć i limit czasu muszą być dobrane do budowy, a nie do trywialnego żądania.

Ta strona jest wyłącznie o natywnym silniku. Mostek Chrome (writeHtmlChrome przez sugerowany pakiet nextpdf/artisan) to inna, cięższa historia: uruchamia headless Chromium przez symfony/process, którego zwykły zip Lambdy ani szczupły kontener nie zawierają. Uruchomienie Chromium na Lambdzie oznacza niestandardową warstwę z przeglądarką i jej bibliotekami współdzielonymi, znacznie większe paczki i znacznie dłuższe zimne starty — poza zakresem tutaj. Sam silnik nie potrzebuje nic z tego.

Zanim zaczniesz, potwierdź, że te elementy są na swoim 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 licencję na ich osadzenie.
  • Masz łańcuch narzędzi dla swojego celu — CLI Bref i framework serverless dla Lambdy lub build kontenera dla Cloud Run / App Runner.

Czytając wprost z pakietu, nextpdf/core wymaga php: >=8.4 <9.0 oraz niewielkiego zestawu rozszerzeń PHP — ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib oraz ext-curl. Standardowe warstwy PHP Bref dołączają każde z nich. Oficjalne obrazy kontenerowe php:8.4 dostarczają openssl, curl i zlib od razu, ale mbstring, gd i intl nie są dołączone — wymagają zainstalowania zależności systemowych i włączenia rozszerzeń za pomocą docker-php-ext-install (zobacz przewodnik wdrażania w Dockerze). W Bref nie ma nic egzotycznego do kompilacji; na ścieżce kontenerowej włączasz te trzy rozszerzenia w buildzie obrazu dla samego silnika.

To, co czyni dopasowanie czystym, to to, czego silnik nie robi:

  • Brak podprocesu dla ścieżki rdzeniowej. Budowanie dokumentu i wywołanie getPdfData() to PHP działający w procesie od początku do końca. Zależność symfony/process istnieje dla opcjonalnego mostka Chrome, a nie dla natywnego renderowania — natywne generowanie PDF nigdy nie uruchamia procesu.
  • Brak stanu trwałego. Każde wywołanie buduje świeży dokument i zwraca bajty. Nic nie musi przetrwać między żądaniami poza ciepłym kontenerem, który wykorzystujesz do rozgrzewania czcionek (poniżej), ale nigdy nie polegasz na nim dla poprawności.
  • Brak potrzeby zapisywalnego katalogu roboczego. Silnik buduje PDF w pamięci i zwraca go jako łańcuch znaków; dotyka dysku tylko wtedy, gdy ty wywołasz save(). Na serverless tego nie robisz — zwracasz bajty — więc brak trwałego systemu plików nigdy nie gryzie ścieżki budowy.

Jedno twarde ograniczenie: brak trwałego, zapisywalnego systemu plików

Dział zatytułowany „Jedno twarde ograniczenie: brak trwałego, zapisywalnego systemu plików”

System plików wdrożenia nie jest trwały, ale model różni się w zależności od środowiska. AWS Lambda gwarantuje jedynie zapisywalny /tmp (domyślnie 512 MB, konfigurowalny do 10 GB); reszta systemu plików funkcji jest tylko do odczytu. Środowiska kontenerowe (Cloud Run, App Runner) mają ulotny, ograniczony do kontenera zapisywalny system plików zamiast modelu wyłącznie /tmp — ale wszystko, co tam zapisane, jest tracone, gdy kontener jest przetwarzany ponownie, więc to przestrzeń robocza, a nie magazyn. W każdym przypadku preferuj /tmp lub skonfigurowany wolumen do przygotowywania i nigdy nie polegaj na zapisach do ścieżki obrazu aplikacji jako na trwałym magazynie. Wynikają z tego dwie konsekwencje.

Nigdy nie wywołuj save() w oczekiwaniu na trwały wynik. NextPDF\Core\Document udostępnia zarówno save(string $path): void, jak i getPdfData(): string. Na serverless używasz getPdfData() i zwracasz lub przesyłasz bajty — nie traktuj zapisu do katalogu aplikacji jako trwałego magazynu. Jeśli musisz przygotować plik (na przykład do przesłania wieloczęściowego do magazynu obiektów), zapisz pod /tmp (lub skonfigurowanym wolumenem) i posprzątaj, pamiętając, że na ciepłym kontenerze ta przestrzeń robocza przetrwa między wywołaniami i liczy się do jej limitu rozmiaru.

use NextPDF\Core\Document;
// Right for serverless: get the bytes, return or upload them.
$pdf = $document->getPdfData(); // string of PDF bytes, built in memory
// Avoid on serverless: save() writes to disk. On Lambda the application
// directory is read-only; on Cloud Run / App Runner it is writable but
// ephemeral (lost on container recycle). Neither is durable storage.
// $document->save('/var/task/out.pdf'); // not durable — return the bytes instead

Nie instaluj czcionek systemu operacyjnego w czasie działania i nie polegaj na automatycznym wykrywaniu czcionek; spakuj swoje pliki czcionek na produkcję. Na Lambdzie system plików tylko do odczytu blokuje apt-get install fonts-* całkowicie; na środowisku kontenerowym każda instalacja w czasie działania ląduje na ulotnym systemie plików i jest tracona przy następnym przetworzeniu. I tak by to nie pomogło, bo natywny silnik nie odczytuje żadnych czcionek OS/fontconfig — rozpoznaje czcionki wyłącznie z plików, które zarejestrujesz. Dlatego na produkcję pliki czcionek muszą znajdować się wewnątrz artefaktu wdrożeniowego. Jeśli celowo pobierasz pliki czcionek do /tmp lub skonfigurowanego wolumenu, musisz jawnie zarejestrować je w rejestrze czcionek i zaakceptować dodatkowy koszt zimnego startu oraz niezawodności — to nie jest zalecany wzorzec produkcyjny.

Natywny silnik rozpoznaje czcionki z plików czcionek przez NextPDF\Typography\FontRegistry, a nie z fontconfig ani czcionek zainstalowanych w systemie operacyjnym. Na serverless to nie podlega negocjacji: nie ma trwałego systemu plików, na którym można umieścić czcionki po wdrożeniu, więc podróżują wewnątrz paczki (zip lub warstwy Lambdy) albo wewnątrz obrazu (Cloud Run / App Runner).

Spakuj swoje pliki .ttf / .otf / .ttc pod katalogiem w projekcie — resources/fonts/ to konwencja — tak aby zostały włączone do artefaktu. Następnie zarejestruj ten katalog w PHP. Silnik nie odczytuje żadnej zmiennej środowiskowej ze ścieżką 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ę z frameworkiem, a nie przez nextpdf/core. Sama funkcja musi zbudować rejestr z dołączonym katalogiem:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Register the directory the deployment artifact bundled the fonts into.
// On Lambda/Bref the code root is /var/task; adjust for your runtime.
$registry = new FontRegistry(__DIR__ . '/resources/fonts');
// (equivalently, $registry->addFontDirectory(__DIR__ . '/resources/fonts');)
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$document = $factory->create();

To cała kwestia serverless dla czcionek. Reguły nazewnictwa plików, pełne API rejestru oraz obsługa nietrwałego systemu plików znajdują się na dedykowanej stronie — nie powielaj ich tutaj. Przeczytaj Udostępnianie czcionek dla natywnego silnika na produkcji po pełny wzorzec i zarejestruj ten sam katalog, który spakowałeś. Przewodnik wdrażania w Dockerze omawia równoważne pakowanie po stronie obrazu dla przypadku Cloud Run / App Runner.

Zimny start płaci za bootstrap PHP, zoptymalizowany autoloader Composera oraz ewentualne parsowanie czcionek, które uruchamia pierwsza budowa. Nie możesz uniknąć bootstrapu, ale możesz przenieść pracę z czcionkami poza gorącą ścieżkę i wykorzystać ją ponownie między ciepłymi wywołaniami.

Zbuduj FontRegistry i DocumentFactory raz, poza handlerem, tak aby żyły przez całe życie kontenera i były wykorzystywane ponownie przy każdym ciepłym wywołaniu. Opcjonalnie wywołaj warmup() z plikami czcionek, o których wiesz, że ich użyjesz, tak aby zostały sparsowane podczas inicjalizacji, a nie przy pierwszym renderowaniu, a następnie lock() rejestru, aby jego sparsowany stan był zamrożony i żadna mutacja przy wywołaniu nie mogła się ścigać:

use NextPDF\Typography\FontRegistry;
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
// Container-scoped, built once at cold start (module scope, not per request).
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
// Parse the fonts you will actually use now, so the first render does not.
$registry->warmup([
$fontsDir . '/liberation/LiberationSans-Regular.ttf',
$fontsDir . '/liberation/LiberationSans-Bold.ttf',
]);
// Freeze the parsed state for the life of the warm container.
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// Each invocation: fresh document from the shared, warm factory.
$handler = static function (array $event) use ($factory): string {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Hello from serverless', newLine: true);
return $document->getPdfData();
};

Wywołaj warmup() przed lock() — rejestr jest zamrażany po zablokowaniu, więc warmup po tym podnosi błąd konfiguracji. Traktuj czcionkę, która nie wczytuje się przy rozgrzewaniu, jako błąd na etapie wdrożenia, a nie szczegół czasu działania: zweryfikuj, że każda ścieżka czcionki, którą zamierzasz rozgrzać, faktycznie istnieje i parsuje się przy starcie, i przerwij wdrożenie (lub swój health check), jeśli któraś nie, zamiast pozwolić, by błędnie wpisana ścieżka ujawniła się później jako brakujące glify. Utrzymuj listę rozgrzewania ograniczoną do czcionek, których potrzebuje typowe wywołanie; rozgrzewanie dużej rodziny, której rzadko używasz, jedynie wydłuża każdy zimny start.

Bref dostarcza środowisko PHP dla Lambdy jako opublikowaną warstwę oraz plugin serverless.yml. Środowisko php-84 już dostarcza rozszerzenia, których potrzebuje nextpdf/core, więc wdrażasz swój kod i czcionki i kierujesz funkcję na handler. Minimalny serverless.yml:

service: nextpdf-serverless
provider:
name: aws
region: us-east-1
runtime: provided.al2023
plugins:
- ./vendor/bref/bref
functions:
generate:
handler: handler.php
description: Generate a PDF with the native NextPDF engine
runtime: php-84
memorySize: 1024 # size to the build; see "Sizing" below
timeout: 30 # seconds; raise for large documents
# The Lambda filesystem is read-only except /tmp. Fonts ship in the
# package under resources/fonts and are registered in the handler.

Handler buduje dokument z ciepłą, ograniczoną do kontenera fabryką i zwraca bajty. Dla API HTTP zwróć je zakodowane w base64 z typem zawartości application/pdf, aby API Gateway traktował treść jako binarną; dla wyzwalacza invoke lub kolejki prześlij bajty do magazynu obiektów i zwróć klucz:

handler.php (outline)
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
use NextPDF\Typography\FontRegistry;
// --- Cold-start: built once per container, reused across warm invocations. ---
$fontsDir = __DIR__ . '/resources/fonts';
$registry = new FontRegistry($fontsDir);
$registry->warmup([$fontsDir . '/liberation/LiberationSans-Regular.ttf']);
$registry->lock();
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
// --- Per-invocation handler. ---
return static function (array $event) use ($factory): array {
$document = $factory->create();
$document->addPage();
$document->cell(0, 10, 'Invoice', newLine: true);
// getPdfData() materializes the whole PDF in memory and returns it.
$bytes = $document->getPdfData();
return [
'statusCode' => 200,
'isBase64Encoded' => true,
'headers' => ['Content-Type' => 'application/pdf'],
'body' => base64_encode($bytes),
];
};

Zweryfikuj, że paczka zawiera zdrowe środowisko, zanim skierujesz na nią ruch. nextpdf/core dostarcza CLI zainstalowane w vendor/bin/nextpdf, którego polecenie doctor raportuje dokładnie te rozszerzenia, których potrzebuje silnik. Uruchom je raz względem tego samego obrazu lub warstwy środowiska, aby potwierdzić, że PHP 8.4 i każde wymagane rozszerzenie jest obecne.

Cloud Run i App Runner uruchamiają kontener, a nie spakowaną funkcję, więc buildem jest obraz Dockera z Konteneryzacji aplikacji NextPDF, a nie paczka Bref. Ograniczenia natywnego silnika są identyczne: spakuj czcionki do obrazu, zarejestruj spakowany katalog w PHP, uruchamiaj bez uprawnień i traktuj system plików jako nietrwały. W odróżnieniu od modelu wyłącznie /tmp Lambdy, kontener Cloud Run / App Runner ma ulotny, ograniczony do kontenera zapisywalny system plików — ale jest on resetowany przy każdym przetworzeniu, więc używaj /tmp (tmpfs na Cloud Run) lub skonfigurowanego wolumenu jako przestrzeni roboczej i nigdy nie polegaj na zapisach do ścieżki obrazu aplikacji jako na trwałym magazynie.

Różnice względem Lambdy są operacyjne, a nie strukturalne:

  • Kontener może pozostać ciepły między żądaniami przy ustawieniu współbieżności, więc rozgrzewanie ograniczonego do kontenera FontRegistry/DocumentFactory powyżej opłaca się przez wiele żądań, a nie tylko przy następnym wywołaniu.
  • Serwujesz przez HTTP (SAPI FPM lub wbudowanego serwera PHP), a nie zdarzenie invoke, więc zwracasz bajty przez odpowiedź swojego frameworka. Dla dużego dokumentu zwróć je jako odpowiedź strumieniowaną — zobacz Strumieniowe przesyłanie dużego wygenerowanego PDF jako odpowiedź HTTP.
  • Limit czasu żądania i pamięć są ustawiane na usłudze (limit czasu / pamięć usługi Cloud Run; konfiguracja instancji App Runner), a nie per funkcja.

Wszystko inne — zestaw rozszerzeń, rejestracja czcionek, wywołanie wyjściowe getPdfData() — to ten sam kod co handler Lambdy.

  • Rozmiar paczki i obrazu. Artefakt niesie vendor/ (tylko produkcja — instaluj z --no-dev) oraz spakowane czcionki. Czcionki dominują: pełna rodzina CJK to dziesiątki megabajtów. Dostarczaj tylko czcionki, które faktycznie renderujesz, aby utrzymać paczkę Lambdy poniżej jej limitów i obraz mały, co także skraca zimne starty. Dołączona rodzina Liberation (resources/fonts/liberation/) jest mała i pokrywa kompatybilne metrycznie podstawienie Helvetiki.
  • Pamięć. getPdfData() buduje cały dokument w pamięci i zwraca go jako jeden łańcuch znaków, więc szczytowa pamięć to z grubsza rozmiar jednego gotowego PDF plus zestaw roboczy budowy. Dobierz pamięć funkcji/kontenera do największego dokumentu, jaki generujesz, a nie do średniego. Na Lambdzie pamięć skaluje także CPU, więc więcej pamięci często oznacza szybszą budowę i tańsze uruchomienie mimo wyższej stawki za milisekundę — zmierz obie wartości. Kilkustronicowy dokument jest wygodny przy 512–1024 MB; dokumenty z dużą ilością obrazów lub wieloma stronami potrzebują więcej.
  • Limit czasu. Budowa, a nie transfer, dominuje budżet żądania. Ustaw limit czasu funkcji powyżej najgorszego przypadku czasu budowy z zapasem. Jeśli dokument jest na tyle duży, by ryzykować przekroczenie limitu, przenieś generowanie na wyzwalacz asynchroniczny (Lambda oparta na kolejce lub zadanie Cloud Run), który zapisuje wynik do magazynu obiektów zamiast blokować żądanie synchroniczne.
  • Rozmiar /tmp. Jeśli przygotowujesz cokolwiek pod /tmp, uwzględnij jego limit rozmiaru i pamiętaj, że przetrwa między ciepłymi wywołaniami — posprzątaj, bo długo żyjący kontener powoli go zapełnia.
  • Brak trwałego save() do katalogu aplikacji. System plików wdrożenia nie jest trwały — katalog aplikacji Lambdy jest tylko do odczytu (zapisy przyjmuje tylko /tmp), a system plików kontenera Cloud Run / App Runner jest zapisywalny, ale ulotny. Użyj getPdfData() i zwróć/prześlij bajty; przygotowuj pod /tmp lub skonfigurowanym wolumenem, jeśli musisz.
  • Nie polegaj na automatycznym wykrywaniu czcionek. Nie instaluj czcionek systemu operacyjnego w czasie działania i nie polegaj na automatycznym wykrywaniu czcionek; spakuj swoje pliki czcionek na produkcję. Natywny silnik nie odczytuje żadnych czcionek OS/fontconfig — rozpoznaje wyłącznie pliki, które zarejestrujesz. Jeśli celowo pobierasz pliki czcionek do /tmp lub skonfigurowanego wolumenu, musisz jawnie zarejestrować je w rejestrze czcionek i zaakceptować dodatkowy koszt zimnego startu oraz niezawodności. Spakuj i zarejestruj pliki. Zobacz stronę o czcionkach linkowaną powyżej.
  • NEXTPDF_FONTS_PATH nic nie robi dla samego silnika. To domyślna wartość konfiguracji nextpdf/laravel, a nie zmienna, którą odczytuje nextpdf/core. Sam handler Bref, który ustawia tylko tę zmienną, nie rejestruje żadnych czcionek i renderuje tofu.
  • Mostek Chrome nie pasuje do zwykłej funkcji. writeHtmlChrome potrzebuje headless Chromium i ścieżki podprocesu symfony/process. Umieszczenie Chromium na Lambdzie wymaga niestandardowej warstwy z przeglądarką i jej bibliotekami, znacznie większych paczek i długich zimnych startów. Natywny silnik i writeHtml nie potrzebują nic z tego — preferuj je na serverless.
  • Koszt zimnego startu to autoload plus parsowanie czcionek. Użyj --optimize-autoloader przy instalacji produkcyjnej i rozgrzej rejestr raz na kontener. Nie rozgrzewaj czcionek, których rzadko używasz.
  • API Gateway wymaga obsługi binarnej. Zwróć isBase64Encoded: true z Content-Type: application/pdf i skonfiguruj API tak, by traktowało application/pdf jako binarny typ mediów, bo inaczej klient otrzyma uszkodzone bajty.
  • Premium i ionCube to cięższa kwestia artefaktu. Buildy NextPDF Pro / Enterprise zakodowane w ionCube potrzebują loadera ionCube dopasowanego do dokładnego buildu PHP w środowisku, którego standardowa warstwa Bref nie zawiera. To poza zakresem wdrożenia serverless dla rdzenia.
  • Nie dostarczaj zależności deweloperskich. Instaluj z --no-dev, aby narzędzia testowe i analizy nigdy nie trafiły do paczki funkcji ani obrazu.
  • Waliduj wejście przed budowaniem. Budowa PDF sterowana wejściem z żądania to wektor wyczerpania pamięci; odrzuć wejścia spoza zakresu lub zbyt duże na granicy, zanim ruszy jakakolwiek praca budowy, i ogranicz współbieżność, aby duży ruch nie zwielokrotnił szczytowej pamięci do awarii braku pamięci.
  • Trzymaj czcionki i licencje poza publicznymi artefaktami. Pakuj tylko czcionki, na które masz licencję, i nigdy nie wbudowuj pliku licencji premium do publicznie wypchniętego obrazu lub warstwy — zamiast tego dostarczaj go w czasie działania przez wartość środowiskową lub menedżer sekretów.
  • Najmniejsze uprawnienia. Nadaj funkcji/usłudze tylko te uprawnienia IAM, których potrzebuje (na przykład dostęp do zapisu do jednego docelowego bucketa), i uruchamiaj kontener bez uprawnień, jak pokazuje przewodnik Dockera.

Ten przewodnik nie formułuje normatywnego twierdzenia o standardach. Fakty platformowe są odczytane bezpośrednio z pakietu nextpdf/core: ograniczenie php: >=8.4 <9.0 oraz wymagane rozszerzenia ext-mbstring, ext-intl, ext-gd, ext-openssl, ext-zlib oraz ext-curl. Standardowa warstwa środowiska Bref PHP-8.4 dostarcza wszystkie sześć; oficjalny obraz php:8.4 dostarcza openssl, curl i zlib, ale mbstring, gd i intl muszą zostać zainstalowane i włączone w buildzie obrazu za pomocą docker-php-ext-install (zobacz stronę Dockera). Wywołanie wyjściowe to rzeczywista powierzchnia rdzenia NextPDF\Core\Document::getPdfData(): string (jego dyskowy odpowiednik to save(string $path): void). Czcionki są rejestrowane przez NextPDF\Typography\FontRegistry — jego argument konstruktora z katalogiem / addFontDirectory(), z warmup(array $fontFiles) i lock() dla wzorca zimnego startu — podpięte przez NextPDF\Core\DocumentFactory::create(). 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. Polecenie doctor CLI nextpdf jest zadeklarowane jako "bin": ["bin/nextpdf"] w pakiecie i instalowane w vendor/bin/nextpdf w aplikacji konsumującej. Nazwy środowisk Bref oraz zachowania AWS Lambda / Cloud Run / App Runner to udokumentowane funkcje tych dostawców.