Exécuter NextPDF sur des plateformes serverless
En un coup d’œil
Section intitulée « En un coup d’œil »Le moteur de cœur NextPDF natif, en processus, est une charge de travail serverless
quasi idéale. C’est du PHP pur exécuté à l’intérieur de ton processus —
composer require nextpdf/core, construis un document, récupère les octets. Il n’y a
aucun binaire externe à lancer, aucun navigateur headless, aucun démon à maintenir en
vie, et aucune socket vers un service annexe. Une fonction qui construit un PDF démarre
à froid, exécute ton PHP, renvoie les octets et se termine. Cela correspond proprement
à AWS Lambda (via le runtime Bref), Google Cloud Run et AWS App Runner.
Cette page couvre le déploiement de ce moteur natif sur ces trois runtimes et le petit ensemble de vraies contraintes qu’ils imposent :
- le système de fichiers du runtime n’est pas durable : Lambda ne garantit qu’un
/tmpaccessible en écriture, tandis que les runtimes en conteneur (Cloud Run, App Runner) ont un système de fichiers éphémère, à portée de conteneur — dans les deux cas les polices doivent voyager à l’intérieur du paquet de déploiement ou de l’image et être enregistrées dans PHP (le moteur ne lit aucune variable d’environnement de chemin de polices) ; - les démarrages à froid paient l’autoloading et tout préchauffage de police, donc
préchauffe la
FontRegistryune fois par conteneur, pas par invocation ; - la taille du paquet, la mémoire et le délai d’expiration doivent être dimensionnés pour le build, pas pour une requête triviale.
Cette page est uniquement pour le moteur natif. Le pont Chrome
(writeHtmlChrome via le paquet suggéré nextpdf/artisan) est une autre histoire, plus
lourde : il fait appel à un Chromium headless via symfony/process, qu’un zip Lambda
ordinaire ou un conteneur allégé ne contient pas. Faire tourner Chromium sur Lambda
implique une couche personnalisée avec le navigateur et ses bibliothèques partagées,
des paquets bien plus volumineux, et des démarrages à froid bien plus longs — hors
périmètre ici. Le moteur nu n’a besoin de rien de tout cela.
Avant de commencer, confirme que ces éléments sont en place :
- Ton application possède un
composer.jsonet uncomposer.lockcommittés, avecnextpdf/corecomme dépendance. - Tu disposes des fichiers de polices que tu comptes embarquer, et tu as le droit de les embarquer.
- Tu disposes de l’outillage pour ta cible — la CLI Bref et le framework
serverlesspour Lambda, ou un build de conteneur pour Cloud Run / App Runner.
Pourquoi le moteur natif convient au serverless
Section intitulée « Pourquoi le moteur natif convient au serverless »Lu directement dans le paquet, nextpdf/core exige php: >=8.4 <9.0 et un petit
ensemble d’extensions PHP — ext-mbstring, ext-intl, ext-gd, ext-openssl,
ext-zlib et ext-curl. Les couches PHP Bref standard les fournissent toutes. Les
images de conteneur officielles php:8.4 fournissent openssl, curl et zlib
d’origine, mais mbstring, gd et intl ne sont pas fournis — ils exigent
d’installer des dépendances système et d’activer les extensions avec
docker-php-ext-install (voir le
guide de déploiement Docker). Sur
Bref il n’y a rien d’exotique à compiler ; sur le chemin conteneur tu actives ces trois
extensions dans le build d’image pour le moteur nu.
Ce qui rend l’adéquation propre, c’est ce que le moteur ne fait pas :
- Aucun sous-processus pour le chemin du cœur. Construire un document et appeler
getPdfData()est du PHP en processus de bout en bout. La dépendancesymfony/processexiste pour le pont Chrome optionnel, pas pour le rendu natif — la génération de PDF native ne lance jamais de processus. - Aucun état persistant. Chaque invocation construit un document neuf et renvoie des octets. Rien ne doit survivre entre les requêtes hormis le conteneur chaud, que tu exploites pour le préchauffage des polices (ci-dessous) mais sur lequel tu ne comptes jamais pour la justesse.
- Aucun répertoire de travail accessible en écriture nécessaire. Le moteur
construit le PDF en mémoire et le renvoie sous forme de chaîne ; il ne touche le
disque que si tu appelles
save(). Sur du serverless tu ne le fais pas — tu renvoies les octets — donc l’absence de système de fichiers durable ne mord jamais le chemin de build.
La seule contrainte dure : pas de système de fichiers durable accessible en écriture
Section intitulée « La seule contrainte dure : pas de système de fichiers durable accessible en écriture »Le système de fichiers de déploiement n’est pas durable, mais le modèle diffère selon
le runtime. AWS Lambda ne garantit qu’un /tmp accessible en écriture (512 Mo par
défaut, configurable jusqu’à 10 Go) ; le reste du système de fichiers de la fonction est
en lecture seule. Les runtimes en conteneur (Cloud Run, App Runner) ont un système de
fichiers éphémère, à portée de conteneur, accessible en écriture plutôt qu’un modèle
/tmp seul — mais tout ce qui y est écrit est perdu quand le conteneur est recyclé,
donc c’est un espace de travail, pas du stockage. Dans tous les cas, préfère /tmp ou
un volume configuré pour la mise en attente, et ne compte jamais sur des écritures vers
le chemin de l’image de l’application comme stockage durable. Deux conséquences en
découlent.
N’appelle jamais save() en t’attendant à une sortie durable.
NextPDF\Core\Document expose à la fois save(string $path): void et
getPdfData(): string. Sur du serverless tu utilises getPdfData() et tu renvoies ou
téléverses les octets — ne traite pas une écriture vers le répertoire de l’application
comme du stockage persistant. Si tu dois mettre un fichier en attente (par exemple,
pour un téléversement multipart vers du stockage objet), écris sous /tmp (ou un volume
configuré) et nettoie, en te rappelant que sur un conteneur chaud cet espace de travail
persiste entre les invocations et compte contre sa limite de taille.
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 insteadN’installe pas de polices OS au runtime, et ne compte pas sur la découverte
automatique de polices ; empaquette tes fichiers de polices pour la production. Sur
Lambda le système de fichiers en lecture seule bloque apt-get install fonts-*
purement et simplement ; sur un runtime en conteneur toute installation au runtime
atterrit sur un système de fichiers éphémère et est perdue au prochain recyclage. Et ça
n’aiderait pas de toute façon, parce que le moteur natif ne lit aucune police
OS/fontconfig — il ne résout les polices qu’à partir des fichiers que tu enregistres.
Donc pour la production les fichiers de polices doivent être livrés à l’intérieur de
l’artefact de déploiement. Si tu récupères délibérément des fichiers de polices dans
/tmp ou un volume configuré, tu dois les enregistrer explicitement auprès du registre
de polices et accepter le coût supplémentaire de démarrage à froid et de fiabilité —
ce n’est pas un schéma de production recommandé.
Empaqueter et enregistrer les polices dans le paquet ou l’image
Section intitulée « Empaqueter et enregistrer les polices dans le paquet ou l’image »Le moteur natif résout les polices à partir de fichiers de polices via la
NextPDF\Typography\FontRegistry, pas à partir de fontconfig ni de polices
installées par l’OS. Sur du serverless c’est non négociable : il n’y a pas de système
de fichiers persistant où placer les polices après le déploiement, donc elles sont
livrées à l’intérieur du paquet (un zip ou une couche Lambda) ou à l’intérieur de
l’image (Cloud Run / App Runner).
Empaquette tes fichiers .ttf / .otf / .ttc sous un répertoire de ton projet —
resources/fonts/ est la convention — pour qu’ils soient inclus dans l’artefact.
Enregistre ensuite ce répertoire dans PHP. Le moteur ne lit aucune variable
d’environnement de chemin 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. Une fonction nue doit construire le
registre avec le répertoire empaqueté :
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();C’est toute la préoccupation serverless pour les polices. Les règles de nommage des fichiers, l’API complète du registre et la gestion du système de fichiers non durable résident sur la page dédiée — ne les duplique pas ici. Lis Provisionner les polices pour le moteur natif en production pour le schéma complet, et enregistre le même répertoire que celui que tu as empaqueté. Le guide de déploiement Docker couvre l’empaquetage équivalent côté image pour le cas Cloud Run / App Runner.
Démarrages à froid : préchauffe la FontRegistry une fois par conteneur
Section intitulée « Démarrages à froid : préchauffe la FontRegistry une fois par conteneur »Un démarrage à froid paie l’amorçage PHP, l’autoloader optimisé de Composer et toute analyse de police déclenchée par le premier build. Tu ne peux pas éviter l’amorçage, mais tu peux sortir le travail sur les polices du chemin chaud et le réutiliser entre les invocations chaudes.
Construis la FontRegistry et la DocumentFactory une fois, hors du handler, pour
qu’elles vivent le temps du conteneur et soient réutilisées à chaque invocation chaude.
Optionnellement, appelle warmup() avec les fichiers de polices que tu sais que tu vas
utiliser, pour qu’ils soient analysés pendant l’initialisation plutôt qu’au premier
rendu, puis lock() le registre pour que son état analysé soit gelé et qu’aucune
mutation par invocation ne puisse créer de course :
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();};Appelle warmup() avant lock() — le registre est gelé une fois verrouillé, donc
un warmup après cela lève une erreur de configuration. Traite une police qui échoue au
chargement au warmup comme une erreur au moment du déploiement, pas un détail
d’exécution : valide que chaque chemin de police que tu comptes préchauffer existe
réellement et s’analyse au démarrage, et fais échouer le déploiement (ou ton
health check) si l’un d’eux ne le fait pas, plutôt que de laisser un chemin mal saisi
remonter plus tard sous forme de glyphes manquants. Garde la liste de warmup aux polices
dont une invocation typique a besoin ; préchauffer une grande famille que tu utilises
rarement ne fait qu’allonger chaque démarrage à froid.
Une fonction Bref sur AWS Lambda
Section intitulée « Une fonction Bref sur AWS Lambda »Bref fournit le runtime PHP pour Lambda sous forme de couche
publiée et de plugin serverless.yml. Le runtime php-84 livre déjà les extensions
dont nextpdf/core a besoin, donc tu déploies ton code et tes polices et tu pointes une
fonction vers un handler. Un serverless.yml minimal :
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.Le handler construit le document avec la fabrique chaude, à portée de conteneur, et
renvoie les octets. Pour une API HTTP, renvoie-les encodés en base64 avec le type de
contenu application/pdf pour qu’API Gateway traite le corps comme binaire ; pour un
déclencheur invoke ou file d’attente, téléverse les octets vers du stockage objet et
renvoie la clé :
<?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), ];};Vérifie que le paquet contient un environnement sain avant d’y router du trafic.
nextpdf/core livre une CLI installée à vendor/bin/nextpdf dont la commande doctor
rend compte exactement des extensions dont le moteur a besoin. Exécute-la une fois
contre la même image ou couche de runtime pour confirmer que PHP 8.4 et chaque extension
requise sont présents.
Cloud Run et App Runner
Section intitulée « Cloud Run et App Runner »Cloud Run et App Runner exécutent un conteneur plutôt qu’une fonction zippée, donc
le build est l’image Docker de
Conteneuriser une application NextPDF,
pas un paquet Bref. Les contraintes du moteur natif sont identiques : empaquette les
polices dans l’image, enregistre le répertoire empaqueté dans PHP, exécute sans
privilèges, et traite le système de fichiers comme non durable. Contrairement au modèle
/tmp seul de Lambda, un conteneur Cloud Run / App Runner a un système de fichiers
éphémère, à portée de conteneur, accessible en écriture — mais il est réinitialisé à
chaque recyclage, donc utilise /tmp (un tmpfs sur Cloud Run) ou un volume configuré
pour l’espace de travail et ne compte jamais sur des écritures vers le chemin de l’image
de l’application comme stockage durable.
Les différences avec Lambda sont opérationnelles, pas structurelles :
- Le conteneur peut rester chaud entre les requêtes sous un réglage de concurrence,
donc le warmup de la
FontRegistry/DocumentFactoryà portée de conteneur ci-dessus est rentable sur de nombreuses requêtes, pas seulement la prochaine invocation. - Tu sers en HTTP (une SAPI FPM ou serveur PHP intégré) plutôt qu’un événement invoke, donc tu renvoies les octets via la réponse de ton framework. Pour un gros document, renvoie-les sous forme de réponse en flux — voir Diffuser en flux un grand PDF généré comme réponse HTTP.
- Le délai d’expiration de requête et la mémoire sont définis sur le service (délai / mémoire du service Cloud Run ; configuration d’instance App Runner) plutôt que par fonction.
Tout le reste — l’ensemble d’extensions, l’enregistrement des polices, l’appel de sortie
getPdfData() — est le même code que le handler Lambda.
Dimensionnement : paquet, mémoire et délai d’expiration
Section intitulée « Dimensionnement : paquet, mémoire et délai d’expiration »- Taille du paquet et de l’image. L’artefact porte
vendor/(production uniquement — installe avec--no-dev) et tes polices empaquetées. Les polices dominent : une famille CJK complète fait des dizaines de mégaoctets. Ne livre que les polices que tu rends réellement pour garder le paquet Lambda sous ses limites et l’image petite, ce qui raccourcit aussi les démarrages à froid. La famille Liberation empaquetée (resources/fonts/liberation/) est petite et couvre la substitution Helvetica à métriques compatibles. - Mémoire.
getPdfData()construit le document entier en mémoire et le renvoie sous forme d’une seule chaîne, donc le pic de mémoire est à peu près la taille d’un PDF fini plus l’ensemble de travail du build. Dimensionne la mémoire de la fonction/conteneur sur le plus grand document que tu génères, pas sur une moyenne. Sur Lambda, la mémoire échelonne aussi le CPU, donc plus de mémoire signifie souvent un build plus rapide et une exécution moins chère malgré le taux par milliseconde plus élevé — mesure les deux. Un document de quelques pages est confortable à 512–1024 Mo ; les documents lourds en images ou à nombreuses pages ont besoin de plus. - Délai d’expiration. Le build, pas le transfert, domine le budget de requête. Définis le délai d’expiration de la fonction au-dessus du temps de build du pire cas avec de la marge. Si un document est assez grand pour risquer un délai dépassé, déplace la génération vers un déclencheur asynchrone (un Lambda adossé à une file ou un job Cloud Run) qui écrit le résultat vers du stockage objet au lieu de bloquer une requête synchrone.
- Taille de
/tmp. Si tu mets quoi que ce soit en attente sous/tmp, tiens compte de sa limite de taille et rappelle-toi qu’il persiste entre les invocations chaudes — nettoie, sinon un conteneur à longue durée de vie le remplit lentement.
Cas limites et pièges
Section intitulée « Cas limites et pièges »- Pas de
save()durable vers le répertoire de l’app. Le système de fichiers de déploiement n’est pas durable — le répertoire de l’app de Lambda est en lecture seule (seul/tmpaccepte les écritures), et un système de fichiers de conteneur Cloud Run / App Runner est accessible en écriture mais éphémère. UtilisegetPdfData()et renvoie/téléverse les octets ; mets en attente sous/tmpou un volume configuré si tu dois. - Ne compte pas sur la découverte automatique de polices. N’installe pas de polices
OS au runtime, et ne compte pas sur la découverte automatique de polices ; empaquette
tes fichiers de polices pour la production. Le moteur natif ne lit aucune police
OS/fontconfig — il ne résout que les fichiers que tu enregistres. Si tu récupères
délibérément des fichiers de polices dans
/tmpou un volume configuré, tu dois les enregistrer explicitement auprès du registre de polices et accepter le coût supplémentaire de démarrage à froid et de fiabilité. Empaquette et enregistre les fichiers. Voir la page polices liée ci-dessus. NEXTPDF_FONTS_PATHne fait rien pour le moteur nu. C’est la valeur par défaut de config denextpdf/laravel, pas une variable quenextpdf/corelit. Un handler Bref nu qui ne définit que cette variable n’enregistre aucune police et rend du tofu.- Le pont Chrome ne convient pas à une fonction ordinaire.
writeHtmlChromea besoin d’un Chromium headless et du chemin de sous-processussymfony/process. Mettre Chromium sur Lambda exige une couche personnalisée avec le navigateur et ses bibliothèques, des paquets bien plus volumineux et de longs démarrages à froid. Le moteur natif etwriteHtmln’ont besoin de rien de tout cela — préfère-les sur du serverless. - Le coût de démarrage à froid, c’est l’autoload plus l’analyse des polices. Utilise
--optimize-autoloaderà l’installation de production et préchauffe le registre une fois par conteneur. Ne préchauffe pas les polices que tu utilises rarement. - API Gateway a besoin d’un traitement binaire. Renvoie
isBase64Encoded: trueavecContent-Type: application/pdf, et configure l’API pour traiterapplication/pdfcomme un type de média binaire, sinon le client reçoit des octets corrompus. - Premium et ionCube sont une préoccupation d’artefact plus lourde. Les builds NextPDF Pro / Enterprise encodés en ionCube ont besoin du ionCube Loader assorti au build PHP exact du runtime, qu’une couche Bref standard n’inclut pas. C’est hors périmètre pour un déploiement serverless du cœur.
Notes de sécurité
Section intitulée « Notes de sécurité »- Ne livre aucune dépendance de dev. Installe avec
--no-devpour que l’outillage de test et d’analyse n’entre jamais dans le paquet ou l’image de la fonction. - Valide l’entrée avant de construire. Un build PDF piloté par l’entrée d’une requête est un vecteur d’épuisement de mémoire ; rejette les entrées hors plage ou surdimensionnées à la frontière avant tout travail de build, et borne la concurrence pour qu’un fort trafic ne multiplie pas le pic de mémoire en une panne de dépassement de mémoire.
- Garde les polices et les licences hors des artefacts publics. N’empaquette que les polices que tu as le droit d’embarquer, et n’incorpore jamais un fichier de licence premium dans une image ou couche poussée publiquement — fournis-le au runtime via une valeur d’environnement ou un gestionnaire de secrets à la place.
- Moindre privilège. Ne donne à la fonction/au service que les permissions IAM dont il a besoin (par exemple, l’accès en écriture au seul bucket de sortie), et exécute le conteneur sans privilèges comme le montre le guide Docker.
Conformité
Section intitulée « Conformité »Ce guide ne fait aucune revendication normative de standards. 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 couche de runtime PHP-8.4 Bref standard fournit les six ;
l’image officielle php:8.4 fournit openssl, curl et zlib, mais mbstring, gd
et intl doivent être installés et activés dans le build d’image avec
docker-php-ext-install (voir la page Docker). L’appel de sortie est la vraie surface
du cœur NextPDF\Core\Document::getPdfData(): string (son pendant disque est
save(string $path): void). Les polices sont enregistrées via
NextPDF\Typography\FontRegistry — son argument de constructeur de répertoire /
addFontDirectory(), avec warmup(array $fontFiles) et lock() pour le schéma de
démarrage à froid — câblées via NextPDF\Core\DocumentFactory::create().
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. La commande doctor de la CLI nextpdf est déclarée comme
"bin": ["bin/nextpdf"] dans le paquet et installée à vendor/bin/nextpdf dans une
application consommatrice. Les noms de runtime Bref et les comportements AWS Lambda /
Cloud Run / App Runner sont les fonctionnalités documentées de ces fournisseurs.
Voir aussi
Section intitulée « Voir aussi »- Conteneuriser une application NextPDF : l’image de production utilisée pour les cibles Cloud Run / App Runner.
- Provisionner les polices pour le moteur natif en production : le nommage des fichiers de polices, l’API du registre et le schéma warmup-et-lock sur lesquels cette page s’appuie.
- Diffuser en flux un grand PDF généré comme réponse HTTP : le modèle de mémoire pour renvoyer un document construit via HTTP sur Cloud Run / App Runner.
- Rendre en périphérie avec Cloudflare : quand une fonction en processus n’est pas le bon runtime.