Aller au contenu
getnextpdf.com

Exécuter NextPDF sur des plateformes serverless

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 processuscomposer 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 /tmp accessible 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 FontRegistry une 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.json et un composer.lock committés, avec nextpdf/core comme 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 serverless pour Lambda, ou un build de conteneur pour Cloud Run / App Runner.

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épendance symfony/process existe 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 instead

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. 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.

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é :

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),
];
};

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 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.
  • 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 /tmp accepte les écritures), et un système de fichiers de conteneur Cloud Run / App Runner est accessible en écriture mais éphémère. Utilise getPdfData() et renvoie/téléverse les octets ; mets en attente sous /tmp ou 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 /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é. Empaquette et enregistre les fichiers. Voir la page polices liée ci-dessus.
  • NEXTPDF_FONTS_PATH ne fait rien pour le moteur nu. C’est la valeur par défaut de config de nextpdf/laravel, pas une variable que nextpdf/core lit. 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. writeHtmlChrome a besoin d’un Chromium headless et du chemin de sous-processus symfony/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 et writeHtml n’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: true avec Content-Type: application/pdf, et configure l’API pour traiter application/pdf comme 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.
  • Ne livre aucune dépendance de dev. Installe avec --no-dev pour 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.

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.