Запуск NextPDF на serverless-платформах
Нативный движок ядра NextPDF, работающий внутри процесса, — почти идеальная
serverless-нагрузка. Это чистый PHP, работающий внутри вашего процесса —
composer require nextpdf/core, построить документ, получить байты. Нет внешнего двоичного
файла для запуска, нет headless-браузера, нет демона, который нужно держать живым, и нет
сокета к sidecar-сервису. Функция, которая строит PDF, стартует холодной, выполняет ваш
PHP, возвращает байты и завершается. Это чисто ложится на AWS Lambda (через среду
выполнения Bref), Google Cloud Run и AWS App Runner.
Эта страница охватывает развёртывание этого нативного движка в этих трёх средах выполнения и небольшой набор настоящих ограничений, которые они накладывают:
- файловая система среды выполнения недолговечна: Lambda гарантирует только записываемый
/tmp, тогда как контейнерные среды выполнения (Cloud Run, App Runner) имеют эфемерную, ограниченную контейнером файловую систему — в любом случае шрифты должны путешествовать внутри пакета развёртывания или образа и быть зарегистрированы в PHP (движок не читает ни одной переменной окружения с путём к шрифтам); - холодные старты оплачивают автозагрузку и любой прогрев шрифтов, поэтому прогревайте
FontRegistryодин раз на контейнер, а не на каждый вызов; - размер пакета, память и тайм-аут должны быть подобраны под сборку, а не под тривиальный запрос.
Эта страница только для нативного движка. Мост Chrome
(writeHtmlChrome через предлагаемый пакет nextpdf/artisan) — это другая, более тяжёлая
история: он вызывает headless Chromium через symfony/process, которого нет в обычном
zip-пакете Lambda или в облегчённом контейнере. Запуск Chromium на Lambda означает
кастомный слой с браузером и его общими библиотеками, гораздо большие пакеты и намного более
долгие холодные старты — за рамками этой страницы. Голому движку всё это не нужно.
Прежде чем начать, убедитесь, что эти части на месте:
- В вашем приложении закоммичены
composer.jsonиcomposer.lock, сnextpdf/coreв качестве зависимости. - У вас есть файлы шрифтов, которые вы намерены встроить, и у вас есть право встраивать их.
- У вас есть инструментарий для вашей цели — CLI Bref и фреймворк
serverlessдля Lambda, или сборка контейнера для Cloud Run / App Runner.
Почему нативный движок подходит для serverless
Заголовок раздела «Почему нативный движок подходит для serverless»Читая прямо из пакета, nextpdf/core требует php: >=8.4 <9.0 и
небольшой набор расширений PHP — ext-mbstring, ext-intl, ext-gd,
ext-openssl, ext-zlib и ext-curl. Стандартные слои PHP Bref содержат
каждое из них. Официальные контейнерные образы php:8.4 предоставляют openssl,
curl и zlib из коробки, но mbstring, gd и intl не входят в набор —
они требуют установки системных зависимостей и включения расширений
через docker-php-ext-install (см.
руководство по развёртыванию в Docker).
В Bref ничего экзотического компилировать не нужно; на пути контейнера вы включаете
эти три расширения при сборке образа для голого движка.
Чистоту соответствия задаёт то, чего движок не делает:
- Нет подпроцесса для основного пути. Построение документа и вызов
getPdfData()— это PHP внутри процесса от начала до конца. Зависимостьsymfony/processсуществует для опционального моста Chrome, а не для нативной отрисовки — нативная генерация PDF никогда не порождает процесс. - Нет постоянного состояния. Каждый вызов строит свежий документ и возвращает байты. Ничто не должно переживать запросы, кроме тёплого контейнера, который вы используете для прогрева шрифтов (ниже), но никогда не полагаетесь на него ради корректности.
- Не нужен записываемый рабочий каталог. Движок строит PDF в
памяти и возвращает его как строку; он касается диска только если вы вызываете
save(). На serverless вы этого не делаете — вы возвращаете байты — поэтому отсутствие недолговечной файловой системы никогда не вредит пути сборки.
Единственное жёсткое ограничение: нет долговечной записываемой файловой системы
Заголовок раздела «Единственное жёсткое ограничение: нет долговечной записываемой файловой системы»Файловая система развёртывания недолговечна, но модель различается по среде выполнения.
AWS Lambda гарантирует только записываемый /tmp (512 МБ по умолчанию, настраивается
вплоть до 10 ГБ); остальная файловая система функции доступна только для чтения. Контейнерные
среды выполнения (Cloud Run, App Runner) имеют эфемерную, ограниченную контейнером
записываемую файловую систему вместо модели только с /tmp — но всё, что туда записано,
теряется при переработке контейнера, поэтому это рабочее пространство, а не хранилище. В
любом случае предпочитайте /tmp или настроенный том для промежуточного хранения и никогда
не полагайтесь на записи в путь образа приложения как на долговечное хранилище. Из этого
следуют два последствия.
Никогда не вызывайте save(), ожидая долговечного вывода. NextPDF\Core\Document
предоставляет и save(string $path): void, и getPdfData(): string. На serverless вы
используете getPdfData() и возвращаете или загружаете байты — не относитесь к записи в
каталог приложения как к постоянному хранилищу. Если вам нужно временно разместить файл
(например, для multipart-загрузки в объектное хранилище), пишите под /tmp (или
настроенный том) и убирайте за собой, помня, что на тёплом контейнере это рабочее
пространство переживает вызовы и засчитывается в его лимит размера.
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Не устанавливайте шрифты ОС во время выполнения и не полагайтесь на автоматическое
обнаружение шрифтов; упакуйте файлы шрифтов для продакшена. На Lambda файловая система
только для чтения прямо блокирует apt-get install fonts-*; на контейнерной среде
выполнения любая установка во время выполнения попадает на эфемерную файловую систему и
теряется при следующей переработке. И это всё равно не помогло бы, потому что нативный
движок не читает шрифты ОС/fontconfig — он разрешает шрифты только из файлов, которые вы
регистрируете. Поэтому для продакшена файлы шрифтов должны поставляться внутри артефакта
развёртывания. Если вы намеренно загружаете файлы шрифтов в /tmp или настроенный том, вы
должны зарегистрировать их явно в реестре шрифтов и принять добавленные затраты на холодный
старт и надёжность — это не рекомендуемый продакшен-паттерн.
Упаковка и регистрация шрифтов в пакете или образе
Заголовок раздела «Упаковка и регистрация шрифтов в пакете или образе»Нативный движок разрешает шрифты из файлов шрифтов через
NextPDF\Typography\FontRegistry, а не из fontconfig или установленных в ОС шрифтов.
На serverless это не подлежит обсуждению: нет постоянной файловой системы, куда можно
положить шрифты после развёртывания, поэтому они поставляются внутри пакета (zip или слой
Lambda) или внутри образа (Cloud Run / App Runner).
Упакуйте ваши файлы .ttf / .otf / .ttc под каталогом в вашем проекте —
resources/fonts/ — это принятое расположение — чтобы они были включены в артефакт.
Затем зарегистрируйте этот каталог в PHP. Движок не читает ни одной переменной
окружения с путём к шрифтам: NEXTPDF_FONTS_PATH — это значение по умолчанию ключа
конфигурации fonts_path пакета nextpdf/laravel
(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), и оно потребляется только
этой интеграцией с фреймворком, а не nextpdf/core. Голая функция должна
сконструировать реестр с упакованным каталогом:
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();Это вся serverless-забота о шрифтах. Правила именования файлов, полный API реестра и обработка недолговечной файловой системы живут на отдельной странице — не дублируйте их здесь. Прочитайте Подготовку шрифтов для нативного движка в продакшене для полного паттерна и зарегистрируйте тот же каталог, который вы упаковали. Руководство по развёртыванию в Docker охватывает эквивалентную упаковку на стороне образа для случая Cloud Run / App Runner.
Холодные старты: прогревайте FontRegistry один раз на контейнер
Заголовок раздела «Холодные старты: прогревайте FontRegistry один раз на контейнер»Холодный старт оплачивает загрузку PHP, оптимизированный автозагрузчик Composer и любой разбор шрифтов, который запускает первая сборка. Вы не можете избежать загрузки, но вы можете вынести работу со шрифтами из горячего пути и переиспользовать её через тёплые вызовы.
Сконструируйте FontRegistry и DocumentFactory один раз, вне обработчика,
чтобы они жили в течение жизни контейнера и переиспользовались при каждом тёплом
вызове. По желанию вызовите warmup() с файлами шрифтов, которые вы точно будете
использовать, чтобы они были разобраны во время инициализации, а не при первой отрисовке,
затем lock() реестр, чтобы его разобранное состояние было заморожено и никакая мутация
на каждый вызов не могла создать гонку:
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();};Вызывайте warmup() до lock() — реестр заморожен после блокировки, поэтому прогрев
после этого вызывает ошибку конфигурации. Относитесь к шрифту, который не загружается
при прогреве, как к ошибке времени развёртывания, а не детали времени выполнения:
проверяйте, что каждый путь шрифта, который вы намерены прогреть, действительно существует
и разбирается при старте, и проваливайте развёртывание (или вашу проверку здоровья), если
какой-то не разбирается, а не давайте опечатанному пути всплыть позже как недостающие глифы.
Держите список прогрева на шрифтах, которые нужны типичному вызову; прогрев большого
семейства, которым вы редко пользуетесь, просто удлиняет каждый холодный старт.
Функция Bref на AWS Lambda
Заголовок раздела «Функция Bref на AWS Lambda»Bref предоставляет среду выполнения PHP для Lambda как опубликованный
слой и плагин serverless.yml. Среда выполнения php-84 уже поставляет
расширения, которые нужны nextpdf/core, поэтому вы развёртываете код и шрифты и направляете
функцию на обработчик. Минимальный 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.Обработчик строит документ с тёплой, ограниченной контейнером фабрикой и
возвращает байты. Для HTTP API возвращайте их в base64 с
типом содержимого application/pdf, чтобы API Gateway трактовал тело как бинарное; для
триггера invoke или очереди загружайте байты в объектное хранилище и возвращайте ключ:
<?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), ];};Проверьте, что пакет содержит здоровое окружение, прежде чем направлять на него трафик.
nextpdf/core поставляет CLI, устанавливаемый в vendor/bin/nextpdf, чья команда doctor
сообщает именно о тех расширениях, которые нужны движку. Запустите её один раз против
того же образа или слоя среды выполнения, чтобы подтвердить, что PHP 8.4 и каждое требуемое
расширение присутствуют.
Cloud Run и App Runner
Заголовок раздела «Cloud Run и App Runner»Cloud Run и App Runner запускают контейнер, а не упакованную в zip функцию, поэтому
сборка — это образ Docker из
Контейнеризации приложения NextPDF,
а не пакет Bref. Ограничения нативного движка идентичны: упакуйте
шрифты в образ, зарегистрируйте упакованный каталог в PHP, запускайте без привилегий
и относитесь к файловой системе как к недолговечной. В отличие от модели только с /tmp у
Lambda, контейнер Cloud Run / App Runner имеет эфемерную, ограниченную контейнером
записываемую файловую систему — но она сбрасывается при каждой переработке, поэтому
используйте /tmp (tmpfs на Cloud Run) или настроенный том для рабочего пространства и
никогда не полагайтесь на записи в путь образа приложения как на долговечное хранилище.
Отличия от Lambda операционные, а не структурные:
- Контейнер может оставаться тёплым между запросами при настройке concurrency,
поэтому прогрев
FontRegistry/DocumentFactory, ограниченный контейнером, выше окупается через много запросов, а не только следующий вызов. - Вы обслуживаете по HTTP (SAPI FPM или встроенный PHP-сервер), а не событие invoke, поэтому возвращаете байты через ответ вашего фреймворка. Для большого документа возвращайте их как потоковый ответ — см. Потоковая передача большого сгенерированного PDF как ответ HTTP.
- Тайм-аут запроса и память задаются на сервисе (тайм-аут / память сервиса Cloud Run; конфигурация экземпляра App Runner), а не на каждую функцию.
Всё остальное — набор расширений, регистрация шрифтов, выходной вызов getPdfData() —
это тот же код, что и обработчик Lambda.
Подбор размера: пакет, память и тайм-аут
Заголовок раздела «Подбор размера: пакет, память и тайм-аут»- Размер пакета и образа. Артефакт несёт
vendor/(только продакшен — установка с--no-dev) и ваши упакованные шрифты. Шрифты доминируют: полное семейство CJK — это десятки мегабайт. Поставляйте только те шрифты, которые вы действительно отрисовываете, чтобы держать пакет Lambda под его лимитами и образ маленьким, что также сокращает холодные старты. Упакованное семейство Liberation (resources/fonts/liberation/) невелико и покрывает метрически совместимую подстановку Helvetica. - Память.
getPdfData()строит весь документ в памяти и возвращает его как одну строку, поэтому пиковая память — это примерно размер одного готового PDF плюс рабочий набор сборки. Подбирайте память функции/контейнера под самый большой документ, который вы генерируете, а не под средний. На Lambda память также масштабирует CPU, поэтому больше памяти часто означает более быструю сборку и более дешёвый запуск, несмотря на более высокую ставку за миллисекунду — измеряйте и то, и другое. Документ из нескольких страниц комфортно умещается в 512–1024 МБ; документы с большим числом изображений или страниц требуют больше. - Тайм-аут. Сборка, а не передача, доминирует в бюджете запроса. Задайте тайм-аут функции выше времени сборки в худшем случае с запасом. Если документ достаточно большой, чтобы рисковать тайм-аутом, перенесите генерацию на асинхронный триггер (Lambda с очередью или job Cloud Run), который записывает результат в объектное хранилище, а не блокирует синхронный запрос.
- Размер
/tmp. Если вы что-то размещаете под/tmp, учитывайте его лимит размера и помните, что он переживает тёплые вызовы — убирайте за собой, иначе долгоживущий контейнер медленно его заполняет.
Граничные случаи и подводные камни
Заголовок раздела «Граничные случаи и подводные камни»- Нет долговечного
save()в каталог приложения. Файловая система развёртывания недолговечна — каталог приложения Lambda доступен только для чтения (только/tmpпринимает записи), а файловая система контейнера Cloud Run / App Runner записываема, но эфемерна. ИспользуйтеgetPdfData()и возвращайте/загружайте байты; размещайте под/tmpили настроенным томом, если необходимо. - Не полагайтесь на автоматическое обнаружение шрифтов. Не устанавливайте шрифты ОС во
время выполнения и не полагайтесь на автоматическое обнаружение шрифтов; упакуйте файлы
шрифтов для продакшена. Нативный движок не читает шрифты ОС/fontconfig — он разрешает
только файлы, которые вы регистрируете. Если вы намеренно загружаете файлы шрифтов в
/tmpили настроенный том, вы должны зарегистрировать их явно в реестре шрифтов и принять добавленные затраты на холодный старт и надёжность. Упакуйте и зарегистрируйте файлы. См. страницу о шрифтах по ссылке выше. NEXTPDF_FONTS_PATHничего не делает для голого движка. Это значение по умолчанию конфигурацииnextpdf/laravel, а не переменная, которую читаетnextpdf/core. Голый обработчик Bref, который задаёт только эту переменную, не регистрирует ни одного шрифта и отрисовывает “тофу”.- Мост Chrome не умещается в обычную функцию.
writeHtmlChromeнуждается в headless Chromium и пути подпроцессаsymfony/process. Размещение Chromium на Lambda требует кастомного слоя с браузером и его библиотеками, гораздо больших пакетов и долгих холодных стартов. Нативному движку иwriteHtmlне нужно ничего из этого — предпочитайте их на serverless. - Стоимость холодного старта — это автозагрузка плюс разбор шрифтов. Используйте
--optimize-autoloaderпри продакшен-установке и прогревайте реестр один раз на контейнер. Не прогревайте шрифты, которыми редко пользуетесь. - API Gateway нуждается в бинарной обработке. Возвращайте
isBase64Encoded: trueсContent-Type: application/pdfи настройте API трактоватьapplication/pdfкак бинарный медиатип, иначе клиент получит повреждённые байты. - Premium и ionCube — это более тяжёлая забота об артефакте. Сборки NextPDF Pro / Enterprise, закодированные ionCube, нуждаются в загрузчике ionCube, соответствующем точной сборке PHP в среде выполнения, которого стоковый слой Bref не включает. Это за рамками базового serverless-развёртывания core.
Примечания по безопасности
Заголовок раздела «Примечания по безопасности»- Не поставляйте dev-зависимости. Устанавливайте с
--no-dev, чтобы инструментарий тестирования и анализа никогда не попадал в пакет или образ функции. - Проверяйте ввод перед сборкой. Сборка PDF, управляемая вводом запроса, — это вектор исчерпания памяти; отклоняйте выходящие за диапазон или слишком большие входные данные на границе до любой работы по сборке и ограничивайте concurrency, чтобы высокий трафик не множил пиковую память в сбой нехватки памяти.
- Держите шрифты и лицензии вне публичных артефактов. Упаковывайте только те шрифты, которые вы имеете право встраивать, и никогда не запекайте файл лицензии premium в публично выкладываемый образ или слой — вместо этого предоставляйте его во время выполнения через значение окружения или менеджер секретов.
- Наименьшие привилегии. Давайте функции/сервису только те разрешения IAM, которые ей нужны (например, доступ на запись в один выходной bucket), и запускайте контейнер без привилегий, как показывает руководство по Docker.
Соответствие
Заголовок раздела «Соответствие»Это руководство не делает нормативного заявления о стандартах. Факты о платформе читаются
прямо из пакета nextpdf/core: ограничение php: >=8.4 <9.0 и
требуемые расширения ext-mbstring, ext-intl, ext-gd, ext-openssl,
ext-zlib и ext-curl. Стандартный слой среды выполнения Bref PHP-8.4 содержит все
шесть; официальный образ php:8.4 предоставляет openssl, curl и zlib, но
mbstring, gd и intl должны быть установлены и включены при сборке образа
через docker-php-ext-install (см. страницу Docker). Выходной вызов — это реальная
core-поверхность
NextPDF\Core\Document::getPdfData(): string (его дисковый собрат —
save(string $path): void). Шрифты регистрируются через
NextPDF\Typography\FontRegistry — её аргумент-каталог в конструкторе /
addFontDirectory(), с warmup(array $fontFiles) и lock() для
паттерна холодного старта — подключаемый через NextPDF\Core\DocumentFactory::create().
NEXTPDF_FONTS_PATH — это ключ конфигурации fonts_path пакета nextpdf/laravel
(env('NEXTPDF_FONTS_PATH', resource_path('fonts'))), а не переменная,
которую читает nextpdf/core. Команда doctor CLI nextpdf объявлена как
"bin": ["bin/nextpdf"] в пакете и устанавливается в vendor/bin/nextpdf в
потребляющем приложении. Имена сред выполнения Bref и поведение AWS Lambda / Cloud Run /
App Runner — это документированные возможности этих поставщиков.
См. также
Заголовок раздела «См. также»- Контейнеризация приложения NextPDF: продакшен-образ, используемый для целей Cloud Run / App Runner.
- Подготовка шрифтов для нативного движка в продакшене: именование файлов шрифтов, API реестра и паттерн прогрева-и-блокировки, на которые опирается эта страница.
- Потоковая передача большого сгенерированного PDF как ответ HTTP: модель памяти для возврата построенного документа по HTTP на Cloud Run / App Runner.
- Отрисовка на edge с Cloudflare: когда функция внутри процесса — не та среда выполнения.