FrankenPHP: PHP-сервер и worker mode — мастерская и 8 опытов
Разбираем FrankenPHP на восьми настоящих HTTP-опытах: classic и worker, пользовательский контекст, ошибки, PDO и restart. Мастерская, конструктор Caddyfile и проект для Windows/Linux/VPS.

FrankenPHP на практике: PHP и Caddy, classic и worker, состояние между запросами, расширения и развёртывание. Восемь настоящих HTTP-опытов, конструктор Caddyfile и исходники для Windows и Linux.
FrankenPHP: PHP-приложение и веб-сервер в одной поставке
Обычно PHP-сайт обслуживают несколько отдельных частей: веб-сервер принимает HTTP, PHP-FPM исполняет код, конфигурация связывает их между собой. FrankenPHP предлагает другой способ собрать этот путь: PHP работает внутри сервера на базе Caddy. Можно обслуживать обычные PHP-страницы, отдавать статику, настраивать HTTPS и запускать приложение в режиме, где его подготовленные объекты остаются в памяти между запросами.
Главная практическая возможность — выбрать жизненный цикл приложения. В classic код обрабатывает запрос привычным образом, с новым запуском приложения. В worker вы один раз создаёте ядро, контейнер сервисов и другие подходящие объекты, затем передаёте ему последующие запросы. Это даёт возможности для оптимизации, но добавляет ответственность за состояние и совместимость библиотек.
Здесь мы разберём восемь настоящих опытов на небольшом PHP API. На странице можно пройти сохранённые HTTP-запросы, сравнить идентификаторы ядра, увидеть очистку пользовательских данных и создать свой Caddyfile. В архиве находятся исполняемое приложение, тесты, варианты Windows и Linux, Docker Compose и пример развёртывания на VPS.
Где это полезно: API, административные сервисы и существующие PHP-проекты
FrankenPHP можно рассматривать для внутреннего API, небольшого веб-сервиса, административной панели, приложения с серверными шаблонами или проекта на поддерживаемом фреймворке. Для команды, которая хочет упаковать приложение вместе с понятной конфигурацией сервера, полезна возможность собирать единый образ и повторять его в разработке и на Linux.
Worker mode особенно интересно исследовать, когда значительная часть запроса уходит на повторную подготовку приложения: загрузку большого контейнера, создание сервисов, чтение конфигурации. Однако медленный SQL-запрос или внешний API сам по себе не становится быстрым от смены PHP-сервера. Сначала определите, где тратится время, затем измерьте подходящий вариант.
Для существующего проекта разумно начать с classic и проверить маршрутизацию, расширения, загрузки, сессии и фоновые команды. Переход к worker — отдельный этап. Старый код может хранить пользователя в static-поле или предполагать, что всё исчезнет после ответа. Простая замена команды запуска не доказывает, что такой проект уже готов жить в памяти долго.
FrankenPHP, PHP-FPM и встроенный php -S: что меняется
Встроенный сервер php -S удобен для локальной разработки и небольших опытов. Он не является готовой заменой конфигурации production-сервера. PHP-FPM — отдельный менеджер процессов, который обычно принимает FastCGI-запросы от Nginx или другого фронта. FrankenPHP в выбранной архитектуре объединяет HTTP-обслуживание Caddy и исполнение PHP внутри своего runtime.
Это не означает, что существующий Nginx становится обязательным или запрещённым. FrankenPHP может принимать внешний трафик сам, а может находиться за уже настроенным reverse proxy. Главное — понимать, где завершается TLS, кто обслуживает статику и какой компонент определяет исходный IP клиента. Два независимо настроенных сервера не должны случайно бороться за одни и те же порты.
При миграции сравнивайте не только синтаксис конфигурации. Проверьте document root, правила front controller, ограничения тела запроса, таймауты, права записи и журналирование. У нас уже есть подробные материалы о Caddy и Nginx; они помогают разложить серверный путь по понятным обязанностям.
Мини-мастерская: пройти запросы и собрать конфигурацию
В первом блоке выберите один из восьми опытов и переходите между запросами. Для каждого сохранены маршрут, метод, HTTP-статус, тело и локальное время ответа. Поля bootId и handled показывают жизнь нашего Kernel: был ли создан новый объект или тот же экземпляр обработал следующий запрос. RequestId относится к конкретному обращению.
Это записи настоящего локального FrankenPHP, а не выполнение PHP в браузере. Кнопки просматривают сохранённые ответы; сервер сайта не запускает новый worker для каждого посетителя. Версия runtime, платформа и дата видны прямо в мастерской. Небольшие значения времени не предлагаются как сравнительный benchmark: измерения включают локальный клиентский HTTP-путь.
Во втором блоке можно выбрать classic или worker, локальный адрес или домен, число worker и предел запросов до перераздания ядра. Конструктор создаёт Caddyfile для дальнейшего запуска. Он не меняет исторические ответы выше. Некорректные значения блокируют скачивание, а сброс возвращает рабочий исходный вариант. Полный проект позволяет проверить результат уже настоящим FrankenPHP.
Проверенная версия и что лежит в исходниках
В примере закреплён FrankenPHP 1.13.5 с PHP 8.5.12 и Caddy 2.11.9. Для Windows использован официальный native-архив этого релиза, для Linux — официальный образ с подходящим расширением PDO SQLite. Номер PHP важен: расширения и способы настройки зависят от конкретной сборки, а не только от наличия команды php в системе.
Наше приложение не требует пакетов Composer: оно использует обычные классы PHP и PDO. Node.js нужен для сборки статической мастерской и автоматизированного HTTP-прогона; он не исполняет PHP-код. Зависимости интерфейса закреплены lock-файлом, Bootstrap включён локально. Если вы добавляете библиотеки в свой вариант, используйте Composer обычным способом.
Версия 1.13.5 является исправляющим релизом, поэтому для воспроизведения не стоит произвольно брать старый бинарник из случайного примера. Список изменений и официальные файлы доступны в релизе проекта. При обновлении повторяйте тесты и записывайте фактическую комбинацию версий.
frankenphp version
# Пример проверенной комбинации:
# FrankenPHP 1.13.5 · PHP 8.5.12 · Caddy 2.11.9
Каталоги: публичный document root отдельно от исходников
В архиве public содержит только мастерскую: HTML, CSS, JavaScript, JSON-записи и текстовые исходники. Настоящий PHP-сайт находится в site. Kernel расположен в php/Kernel.php за пределами document root, а Caddyfile и инструменты лежат в корне проекта. Такое разделение делает назначение каталогов очевидным и уменьшает вероятность случайно опубликовать конфигурацию.
В рабочем приложении имя публичного каталога может быть public, web или другим. Важен сам принцип: серверу не нужно раздавать корень репозитория вместе с зависимостями, секретами и служебными файлами. Root в конфигурации должен указывать на предназначенную для посетителя часть. Front controller подключает остальной PHP-код через файловую систему.
Мы также отделили исполняемый API от записи для статьи. Публикация статической мастерской не требует запускать FrankenPHP на WordPress-хостинге. Посетитель получает воспроизводимый архив и видит честно обозначенный результат предыдущего выполнения. Это удобный формат для изучения серверного инструмента без предоставления публичного запуска произвольного PHP.
frankenphp-project/
site/index.php # Настоящий front controller / worker
site/asset.txt # Статический файл опыта
php/Kernel.php # Код вне document root
public/ # Статическая мастерская
artifacts/recording.json
Caddyfile.classic
Caddyfile.worker
deploy/ # VPS и systemd
Сначала открыть мастерскую в Windows или Linux
Если вы хотите сначала разобраться с поведением, запустите статический интерфейс. Потребуется Node.js 24.15 или новее; в авторской проверке использовался Node 24.19.0. Npm ci устанавливает версии из lock-файла, test проверяет модель интерфейса и записи, build собирает public. Serve выводит локальный адрес в терминал.
Этот путь не требует устанавливать FrankenPHP. Готовая папка public уже находится в архиве, поэтому её можно раздать и другим HTTP-сервером. Открытие через file:// не подходит, поскольку мастерская получает JSON через fetch. На стороне хостинга JavaScript-модули должны отдаваться с правильным MIME; собранные файлы имеют расширение .js.
В Windows используйте npm.cmd в PowerShell, если политика выполнения скриптов мешает запуску одноимённого PowerShell-wrapper. На Linux команды те же без суффикса. Останавливайте свой локальный сервер Ctrl+C. Просмотр записи не создаёт PHP-процесс и не изменяет конфигурацию уже работающих сайтов.
node --version
npm.cmd ci --ignore-scripts
npm.cmd test
npm.cmd run build
npm.cmd run serve
Windows native: отдельная поставка без замены системного PHP
Для настоящего запуска скачайте frankenphp-windows-x86_64.zip из закреплённого официального релиза 1.13.5. В README приведены URL и проверенная SHA256. Распакуйте весь архив в отдельный каталог tools/frankenphp: рядом с executable находятся необходимые DLL и расширения. Переносить только frankenphp.exe в произвольную папку недостаточно.
Переменная FRANKENPHP_BIN указывает helper на нужный бинарник. Это позволяет оставить установленный PHP из PATH без изменений. Команда verify запускает настоящие HTTP-опыты на свободных loopback-портах и создаёт локальную конфигурацию runtime/php.ini с PDO SQLite. Она передаёт PHPRC только собственным дочерним процессам и не редактирует глобальный php.ini.
Такой способ полезен для сравнения технологий на рабочей машине: вы можете проверить проект, не переключая версии PHP для остальных инструментов. Для другой архитектуры или версии нужно взять соответствующую официальную поставку и заново проверить требования. Контрольная сумма из README относится к конкретному архиву, а не ко всем будущим релизам.
$env:FRANKENPHP_BIN=(Resolve-Path './tools/frankenphp/frankenphp.exe').Path
& $env:FRANKENPHP_BIN version
npm.cmd run verify
# Результат нового прогона: runtime/verification.json
# Публичная запись artifacts/recording.json при verify не меняется.
Windows: ручной запуск API и проверка расширений
После verify можно вручную запустить worker с созданным ini. В текущем терминале задайте PHPRC и собственные XDG-пути в runtime, затем проверьте Caddyfile и запустите сервер. Локальный пример привязан к 127.0.0.1:8420 и явно отключает автоматический HTTPS для этого учебного адреса. Это не настройка доменного VPS.
Во втором терминале запросите customer для anna и ivan, затем products с минимальной ценой. HTTP-ответ показывает фактическую версию PHP и идентификатор Kernel. Если появляется could not find driver, проверяйте PDO SQLite именно у выбранного FrankenPHP. Список модулей другого php.exe из PATH не описывает этот runtime.
В проверенном Windows-архиве можно вызвать находящийся рядом php.exe с параметром -c и локальным ini. Не заменяйте этот способ непроверенной командой frankenphp php-cli -m: для выбранного PHP 8.5 такая передача опций не поддерживается. После опыта завершите свой сервер Ctrl+C; переменные этого терминала не требуется сохранять в систему.
$env:PHPRC=(Resolve-Path './runtime/php.ini').Path
$env:XDG_DATA_HOME=Join-Path (Get-Location) 'runtime/caddy-data'
$env:XDG_CONFIG_HOME=Join-Path (Get-Location) 'runtime/caddy-config'
$env:FP_MODE='worker'
& $env:FRANKENPHP_BIN validate --config Caddyfile.worker --adapter caddyfile
& $env:FRANKENPHP_BIN run --config Caddyfile.worker
# В другом терминале:
Invoke-RestMethod 'http://127.0.0.1:8420/customer?user=anna'
Invoke-RestMethod 'http://127.0.0.1:8420/products?min=10000'
Опыт 1. Classic: новый Kernel на каждый запрос
В classic front controller создаёт Kernel и вызывает обработчик одного запроса. В нашем примере конструктор генерирует bootId, создаёт демонстрационную SQLite в памяти и заполняет три товара. Следующий запрос проходит эту подготовку заново. Поэтому bootId меняется, а handled снова равен единице.
Это удобная отправная точка для переноса существующего PHP-проекта: привычный жизненный цикл проще сопоставить с тем, как приложение работало под PHP-FPM. Однако всё равно нужно проверить расширения, маршрутизацию, переменные окружения и ограничения сервера. Совместимость нельзя вывести из того, что один health endpoint ответил успешно.
В мастерской сохранены три реальных запроса classic. Сравните их с worker в следующем опыте: разница видна в жизни объектов, а не только в случайных миллисекундах. Мы специально не называем один режим быстрее по трём коротким обращениям. Подготовка приложения, нагрузка на базу и окружение должны измеряться отдельно.
{
admin off
auto_https off
frankenphp {
num_threads 2
php_ini display_errors 0
}
}
http://127.0.0.1:8420 {
bind 127.0.0.1
root * site
php_server
}
Опыт 2. Worker: ядро остаётся в памяти
В worker Kernel создаётся перед циклом, а обработчик захватывает ссылку на него. FrankenPHP передаёт последующие запросы в этот handler, сохраняя подготовленное приложение. В записи bootId остаётся прежним, а handled увеличивается. При этом каждый HTTP-запрос получает собственный requestId.
Так можно убрать часть повторной подготовки, если объекты действительно пригодны для длительной жизни. Например, контейнер сервисов может быть неизменяемым, а конфигурация загружается один раз. Но поля, связанные с пользователем, организацией или конкретной формой, требуют отдельного сброса. Само сохранение Kernel ничего не говорит о том, какие его свойства безопасно сохранять.
В нашем опыте намеренно один worker, чтобы последовательность была прозрачной. При нескольких worker запросы могут попадать в разные экземпляры и bootId будет различаться. Это нормальное поведение, которое важно учитывать при отладке. Общий счётчик заказов нельзя хранить только в поле одного Kernel, если он должен быть одинаковым для всего сервиса.
$kernel = new Kernel();
$handler = static function () use ($kernel): void {
$kernel->handle();
};
for ($i = 0; $i < 1000; ++$i) {
$keepRunning = frankenphp_handle_request($handler);
gc_collect_cycles();
if (!$keepRunning) break;
}
Как читать worker-цикл и что происходит после ответа
Вход в worker-скрипт выполняет подготовку приложения, а frankenphp_handle_request вызывает переданный callback при поступлении HTTP-запроса. После завершения callback управление возвращается в цикл. Здесь можно выполнить предусмотренную архитектурой завершающую работу, собрать циклический мусор и решить, нужно ли продолжать.
Не путайте такую завершающую фазу с надёжной очередью. Если процесс завершится, произвольная операция после ответа не получит автоматически повтор и сохранённый статус. Для длительной критичной задачи лучше отдельный worker очереди с понятной моделью доставки. FrankenPHP worker в этой статье обслуживает HTTP, а не заменяет все фоновые механизмы приложения.
Также нужно учитывать способ завершения. Return из обработчика отличается от завершения всего worker-скрипта. Код, который привык использовать exit или die глубоко в библиотеке, требует проверки в новом жизненном цикле. Для существующего фреймворка часто лучше использовать поддерживаемую интеграцию, чем вручную оборачивать любой front controller бесконечным циклом.
Опыт 3. Анна, Иван и очистка состояния между запросами
В опыте isolation через один Kernel последовательно проходят anna, ivan и guest. Обработчик записывает текущего пользователя и временное приветствие, а в finally очищает эти значения. В начале каждого следующего ответа userBefore должен быть null, cacheBefore — ноль. Это проверяется реальными HTTP-утверждениями, а не только текстом статьи.
Query-параметр user здесь является учебным переключателем. Он не выполняет авторизацию и не доказывает права доступа. В рабочем приложении текущая личность появляется после проверки сессии или токена. Именно этот request-scoped контекст нельзя случайно переносить в обработку следующего посетителя.
Практическая ценность опыта в том, что утечка состояния часто незаметна при ручной проверке одним аккаунтом. Всегда чередуйте разных пользователей, tenant и локали в одном долгоживущем процессе. Проверьте не только успешный путь, но и исключение, ранний возврат, ошибку валидации. Finally помогает выполнить очистку для всех этих завершений внутри обработчика.
try {
$this->currentUser = $verifiedUser;
$this->requestCache['greeting'] = $greeting;
// Обработать запрос и сформировать ответ.
} finally {
$this->currentUser = null;
$this->requestCache = [];
}
Что именно нужно сбрасывать: объекты, кеши и транзакции
Runtime готовит окружение нового HTTP-запроса, но ваши долгоживущие объекты не обязаны исчезать. Состояние может находиться в static-полях, singleton-сервисах, контейнере зависимостей, пользовательском кеше, накопленном буфере или открытой транзакции. Без инвентаризации таких мест нельзя считать приложение изолированным только потому, что массив $_GET изменился.
В нашем Kernel дополнительно откатывается незавершённая PDO-транзакция. В настоящем проекте нужно определить владельца транзакции и правила cleanup, проверить восстановление соединения после простоя, сброс tracing-контекста и локали. Некоторые библиотеки предоставляют reset-интерфейсы; другие требуют адаптации или не подходят для worker mode.
Gc_collect_cycles помогает сборщику циклического мусора, но не удаляет объект, на который вы продолжаете ссылаться. Поэтому вызов GC не заменяет очистку requestCache, а MAX_REQUESTS не делает безопасной утечку пользователя между двумя соседними запросами. Эти механизмы решают разные задачи и должны быть видны в коде отдельно.
try {
// Выполнение текущего запроса.
} finally {
if ($this->database->inTransaction()) {
$this->database->rollBack();
}
$this->currentUser = null;
$this->requestCache = [];
}
Опыт 4. JSON API: успешный запрос и ошибки данных
Маршрут orders принимает POST с application/json. Обработчик ограничивает размер прочитанного тела, разбирает JSON с исключениями и проверяет, что корень является объектом, а title — непустой строкой допустимой длины. Успешный запрос возвращает 201, неверные данные — 422, сломанный JSON — 400.
Эти статусы полезно различать и в клиентском интерфейсе. Ошибка синтаксиса запроса требует исправить отправку, ошибка поля — показать конкретное сообщение, временный серверный сбой — дать возможность повторить позже. Один общий ответ «не удалось» скрывает причину и усложняет поддержку API.
Наш ответ явно содержит persisted:false: заказ не сохраняется в базу. Так посетитель видит границу учебного примера. Добавление настоящего хранилища потребует транзакции, обработки повторов и правил доступа. Выбранный веб-сервер не делает эти обязанности автоматически. Если нужны подробные контракты PHP-данных, полезно связать этот пример с Symfony Serializer.
curl -i http://127.0.0.1:8420/orders -H 'Content-Type: application/json' --data '{"title":"Настройка сервиса"}'
curl -i http://127.0.0.1:8420/orders -H 'Content-Type: application/json' --data 'null'
Опыт 5. Исключение и следующий запрос того же worker
Маршрут fail намеренно выбрасывает исключение. Kernel перехватывает Throwable, записывает событие с requestId и классом исключения в журнал, возвращает клиенту короткий INTERNAL_ERROR и выполняет finally. Следующий запрос к customer успешно обрабатывается тем же worker, что подтверждается сохранённым bootId.
Этот опыт демонстрирует обработанную ошибку прикладного кода. Он не доказывает, что процесс восстановится после любого fatal error, ошибки расширения или повреждения состояния. Для настоящего сервиса нужны наблюдение за завершениями worker, ограничения последовательных отказов и понятный health-check. Бесконечный автоматический рестарт не исправляет синтаксическую ошибку в файле.
Клиенту не следует отдавать stack trace, DSN и внутренние пути. Ему нужен безопасный код ошибки и идентификатор обращения, по которому поддержка найдёт подробности в журнале. Важно также проверить очистку контекста именно после исключения: успешный путь и аварийный путь могут оставлять разные следы в долгоживущих сервисах.
try {
// Выполнение текущего запроса.
} catch (Throwable $error) {
error_log(json_encode([
'event' => 'request_failed',
'requestId' => $requestId,
'exception' => $error::class,
], JSON_THROW_ON_ERROR));
$this->json(500, ['error' => 'INTERNAL_ERROR', 'requestId' => $requestId]);
}
Опыт 6. Статический файл и front controller для неизвестного пути
Asset.txt отдаётся как обычный статический ресурс Caddy. В ответе нет полей Kernel, потому что для этого файла не требуется прикладная обработка. Несуществующий маршрут проходит через front controller и получает наш JSON 404. Это позволяет явно увидеть, где работает веб-сервер, а где начинается приложение.
Для настоящего сайта проверьте изображения, CSS, JavaScript, пути вложенных страниц и редиректы каталогов. Ошибка document root часто выглядит как проблема PHP, хотя сервер просто ищет файл не там. В проекте конфигурация отделена от site, поэтому запрос к Caddyfile не должен раскрывать настройки приложения.
Загружаемые пользователями файлы требуют отдельного проектирования: место хранения, допустимые типы, имена и способ отдачи. Не делайте вывод, что любой каталог внутри публичного корня автоматически безопасен для загрузок. В данном учебном API загрузки вообще нет; опыт ограничен доверенным asset.txt и собственными маршрутами.
curl -i http://127.0.0.1:8420/asset.txt
curl -i http://127.0.0.1:8420/not-found
# Первый ответ — текстовый файл, второй — JSON404 приложения.
Опыт 7. PDO, расширения и запрос с параметром
При создании Kernel открывается PDO SQLite в памяти и добавляются три демонстрационных товара. Маршрут products проверяет min как целое число в заданном диапазоне, затем передаёт его в prepared statement. В записи обычный min=10000 возвращает две позиции, а строка с SQL-подобным выражением отклоняется до выполнения запроса.
Это одновременно показывает прикладную проверку и зависимость от расширения конкретного PHP runtime. Composer не устанавливает PDO SQLite внутрь бинарника FrankenPHP. В Docker расширение добавляется при сборке, а в Windows оно подключается из официального native-архива через локальный ini. Подключение неподходящей DLL от другой версии PHP не является заменой этому шагу.
SQLite здесь демонстрационная, а база находится в памяти каждого Kernel. Она исчезает при перераздании worker и не является общим хранилищем нескольких worker. Для подробного изучения PDO и SQLite используйте наш раздел PHP-мануала о параметрах PDO и отдельную мастерскую SQLite.
$query = $pdo->prepare(
'SELECT id, name, price FROM products WHERE price >= :min ORDER BY price'
);
$query->bindValue(':min', $min, PDO::PARAM_INT);
$query->execute();
$items = $query->fetchAll(PDO::FETCH_ASSOC);
Опыт 8. Перераздание worker после нескольких запросов
В последнем опыте переменная MAX_REQUESTS равна трём. Это параметр нашего worker-цикла: после трёх обработок скрипт заканчивается, а FrankenPHP запускает новый worker. В последовательности HTTP-ответов меняется bootId, handled возвращается к небольшому значению. При этом запросы продолжают получать успешные ответы.
Ограничение срока жизни бывает полезно как дополнительная мера против накопления памяти или состояния в сторонних библиотеках. Но его нельзя принимать за доказательство отсутствия утечки. Если процесс растёт слишком быстро или переносит данные пользователя уже в следующий запрос, перезапуск раз в тысячу обращений не решит основную проблему.
Число запросов до перераздания зависит от приложения, памяти, стоимости подготовки и требований к задержке. Три запроса выбраны только для короткого наглядного опыта. В production наблюдайте память во времени, частоту restart и ошибки. Также отличайте наш MAX_REQUESTS от других параметров runtime с похожими названиями: здесь границу явно реализует PHP-цикл из исходников.
# Linux, из корня проекта:
FP_MODE=worker MAX_REQUESTS=3 frankenphp run --config Caddyfile.worker
# Windows PowerShell перед тем же запуском:
# $env:MAX_REQUESTS='3'
Несколько worker: память разделена, ресурсы суммируются
Когда запускается несколько worker, у каждого свой Kernel, свои поля и своё подключение PDO. Запросы могут распределяться между ними, поэтому bootId в соседних ответах не обязан совпадать. Это нормальный результат распределения работы, а не обязательно аварийный restart. Для диагностики нужно смотреть на режим, число worker и журнал событий вместе.
Общие данные следует хранить вне памяти одного worker: в базе, Redis или другом подходящем хранилище. Особенно это касается пользовательских сессий, счётчиков, очередей и состояния операций. Локальный кеш может быть полезен, но его согласованность и очистка требуют отдельного решения.
Количество worker влияет не только на параллелизм. Каждый экземпляр сохраняет подготовленное приложение и соединения, поэтому потребление памяти и нагрузка на базу растут. Предел шестнадцать в конструкторе — ограничение учебной формы, а не готовая рекомендация для VPS. Начните с небольшого числа и измерьте свой сервис при реалистичной нагрузке.
frankenphp {
num_threads 2
worker {
file /srv/frankenphp/site/index.php
num 2
env FP_MODE worker
env MAX_REQUESTS 1000
}
}
Конструктор Caddyfile: что он проверяет и что остаётся настроить
Конструктор принимает только известные режимы, проверяет числовые границы и домен без протокола, пути или порта. В classic поля worker отключены и не мешают скачать конфигурацию. Локальный вариант использует loopback и HTTP; доменный вариант предполагает Linux/VPS и автоматическое HTTPS Caddy.
Полученный файл нужно проверить настоящей командой validate в каталоге проекта. Это обнаруживает синтаксические ошибки и часть проблем конфигурации, но не подтверждает работоспособность DNS, доступность внешних портов или бизнес-логики. Перед запуском замените пути и убедитесь, что на выбранном адресе нет другого сервиса.
Настройки интерфейса не меняют приложенную запись. Если хотите сравнить два числа worker, выполните новые реальные тесты и сохраните отдельные результаты. Для воспроизводимости полезно хранить Caddyfile рядом с версией приложения и фиксировать образ или бинарник, которыми он проверен. Изменение одного поля без контекста runtime не является полноценным экспериментом.
frankenphp validate --config Caddyfile --adapter caddyfile
frankenphp run --config Caddyfile --adapter caddyfile
Linux и Docker Compose: повторяемая поставка приложения
В комплекте есть Dockerfile на основе dunglas/frankenphp:1.13.5-php8.5-bookworm. Он включает production-настройки PHP и PDO SQLite, копирует site и Kernel в отдельные каталоги и запускает worker через Caddyfile.container. Compose публикует сервис только на loopback хоста, чтобы учебный API не открывался всему Интернету.
Проверьте порт 8420 и выделенную подсеть 10.254.244.0/24. Если они пересекаются с вашим окружением, измените конфигурацию до запуска. Отдельное имя Compose project помогает управлять только ресурсами этой лаборатории. Down следует выполнять для своего project, а не останавливать чужие контейнеры или весь Docker.
Исходники в этом варианте копируются в образ. После изменения PHP требуется пересборка и пересоздание контейнера; простое редактирование файла на хосте не обновляет уже запущенный образ. Для разработки можно отдельно настроить bind mount, понимая его влияние. Управление образами и несколькими сервисами подробно разбирается в нашей статье о Docker Compose.
docker compose -p aen-frankenphp-lab up -d --build
curl -fsS http://127.0.0.1:8420/health
docker compose -p aen-frankenphp-lab logs --tail=50 php
# После завершения именно этой лаборатории:
docker compose -p aen-frankenphp-lab down
Linux без Docker: бинарник, пакет и нужные расширения
FrankenPHP можно установить и без контейнера, используя официальную поставку для своей системы. При выборе способа учитывайте расширения: статический бинарник и динамическая сборка имеют разные возможности подключения модулей. Нашему Kernel нужен PDO SQLite, поэтому убедитесь, что он присутствует именно в выбранном runtime.
Команда php в PATH может относиться к системному CLI или другой версии. Аналогично установленное расширение для PHP-FPM не обязано быть доступным FrankenPHP. Проверяйте фактический HTTP-ответ, версию сервера и используемый ini. Удобно запускать verify с явным FRANKENPHP_BIN: тогда в записи видно, какой исполняемый файл использовался.
Для production выбирайте поддерживаемый способ обновления и фиксируйте версию поставки. Если приложение зависит от редкого расширения, заранее проверьте его совместимость с выбранной сборкой. Базовая успешная выдача HTML ещё не подтверждает, что работают обработка изображений, почта, драйверы базы и остальные функции вашего проекта.
frankenphp version
export FRANKENPHP_BIN="$(command -v frankenphp)"
npm ci --ignore-scripts
npm test
npm run verify
FP_MODE=worker APP_PORT=8420 frankenphp run --config Caddyfile.worker
VPS: домен, HTTPS и постоянное хранение сертификатов
Для VPS выберите реальный домен, настройте DNS и замените адрес в deploy/Caddyfile.production. Caddy может обслуживать HTTPS автоматически, когда инфраструктурные условия для этого выполнены. Локальный HTTP-пример с auto_https off нельзя бездумно перенести на публичный домен и считать, что HTTPS уже включён.
Состояние сертификатов должно жить дольше одного пересоздания контейнера или worker. В systemd-примере XDG-пути размещены под /var/lib/frankenphp, а пользователь сервиса имеет право записи только в нужные каталоги. Для контейнерного production нужно отдельно предусмотреть постоянное хранилище данных Caddy. Потеря этих файлов при каждом deploy создаёт ненужные повторные выдачи сертификатов.
На сервере document root остаётся /srv/frankenphp/site. Исходники Kernel и конфигурация находятся снаружи. Проверьте, кто уже слушает 80 и 443: возможно, существующая панель хостинга управляет своим reverse proxy. В таком окружении часто удобнее оставить TLS на имеющемся фронте и подключить FrankenPHP по внутреннему адресу.
app.example.com {
root * /srv/frankenphp/site
encode zstd gzip
log
php_server
}
# Это фрагмент доменного сайта.
# Полный worker/global блок находится в deploy/Caddyfile.production.
Systemd: запуск, журнал и контролируемый restart
Архив содержит пример systemd unit для отдельного пользователя frankenphp. Перед использованием замените путь к бинарнику и убедитесь, что проект находится в указанном каталоге. Unit проверяет Caddyfile перед запуском, хранит служебные данные в StateDirectory и направляет вывод в журнал. Исходники не требуют права записи для пользователя веб-сервера.
Пример подходит нашему приложению с SQLite в памяти. Если рабочему сервису нужны файлы загрузок, кеш или сессии, выделите им отдельные каталоги и согласуйте ограничения unit. Не исправляйте проблему владельца общим chmod 777. Разделение прав помогает понять, какой компонент действительно должен менять конкретные файлы.
Admin API в наших примерах выключен, поэтому способом обновления является контролируемый restart сервиса, а не команда reload через административный порт. После замены приложения проверьте health и журнал. Restart уничтожает память worker; всё, что должно сохраниться, обязано находиться в постоянном хранилище. Автозагрузку включайте после успешной приёмки своего окружения.
sudo systemctl daemon-reload
sudo systemctl start frankenphp-workshop
sudo systemctl status frankenphp-workshop --no-pager
sudo journalctl -u frankenphp-workshop -n 50 --no-pager
# После установки и проверки новой версии приложения:
sudo systemctl restart frankenphp-workshop
Существующий reverse proxy и доверенные заголовки
Если перед FrankenPHP уже стоит Nginx, Caddy или балансировщик, приложение получает запрос через посредника. Нужно определить, каким заголовкам и от каких адресов можно доверять. Без этого легко получить неверное определение HTTPS или IP клиента. Но доверять любому присланному X-Forwarded-For тоже нельзя: внешний отправитель может подставить собственное значение.
Настройка включает обе стороны: транспортный сервер и используемый фреймворк приложения. Их правила должны соответствовать реальной сети. Не копируйте широкий список доверенных диапазонов только ради исчезновения предупреждения. В простом локальном стенде такого прокси нет, поэтому дополнительные доверенные заголовки нам не нужны.
Также согласуйте таймауты и лимиты тела запроса на всех уровнях. Если внешний прокси завершает соединение раньше, приложение может продолжать операцию, хотя клиент уже увидел ошибку. Для изменяющих запросов потребуется идемпотентность и понятная политика повторов. Смена PHP runtime сама по себе не устраняет эти распределённые пограничные ситуации.
Логи и наблюдение: запрос, worker и память во времени
Для расследования ошибки полезно связывать requestId, режим, bootId и событие приложения. В примере requestId появляется в ответе и журнале исключения. В рабочем сервисе обычно не нужно публично показывать все внутренние диагностические поля; достаточно безопасного идентификатора для поддержки.
Наблюдайте число перезапусков, память процесса, задержки, ошибки и загрузку зависимостей. Один скачок памяти не всегда означает утечку, а стабильный рост после одинаковых запросов требует исследования. У нескольких worker значения нужно интерпретировать с учётом отдельных экземпляров, а не считать один Kernel общим для сервера.
FrankenPHP интегрируется с журналированием Caddy; доступны обычные PHP-логи и специальные возможности структурированного вывода. В нашем коде используется стандартный error_log с JSON-контекстом. Для расширения посмотрите официальное описание журналирования. Секреты, пароли и полные токены не должны попадать в диагностический контекст.
error_log(json_encode([
'event' => 'request_completed',
'requestId' => $requestId,
'memoryBytes' => memory_get_usage(true),
], JSON_THROW_ON_ERROR));
Как измерять пользу worker mode без случайных выводов
Сравнивайте одинаковые маршруты и данные при сопоставимом окружении. Отдельно измерьте прогрев, обычную обработку и длинный период работы. Worker может убрать повторную подготовку, но одновременно увеличить постоянное потребление памяти. Если приложение ждёт внешний API, ускорение загрузки Kernel может почти не изменить итоговую задержку.
Для сравнения полезны нагрузочный генератор, серверные метрики и профиль приложения. Смотрите не только на среднее время: хвосты распределения, ошибки и ожидание свободного исполнителя часто важнее. Объём нагрузки должен соответствовать реальной задаче, а не максимальному числу запросов, которое удалось отправить с той же машины.
В нашей статье о k6 показано, как формулировать сценарии и пороги. Здесь короткие измерения используются для доказательства жизненного цикла и статусов, а не для рейтинга серверов. Если вы публикуете результат своего сравнения, приложите версии, конфигурацию, сценарий, данные и условия теста.
Symfony, Laravel и другие готовые приложения
Для популярных фреймворков существуют поддерживаемые способы интеграции с FrankenPHP. Они помогают правильно обрабатывать завершение запроса и сброс сервисов. Используйте документацию именно своей версии фреймворка и адаптера, поскольку детали жизненного цикла могут меняться.
Самостоятельный Kernel в этой статье полезен для понимания механизма: видно, что создаётся один раз, что происходит на каждом запросе и что очищается. Но это не универсальная обёртка для любого проекта. Приложение с глобальным состоянием, сложными сессиями или нестандартными расширениями требует собственной проверки.
Практический путь миграции: сначала запустить существующий проект в classic, пройти функциональные сценарии, затем подключить рекомендованную worker-интеграцию и проверить чередование пользователей, ошибки, транзакции и длительную работу. Сохраняйте возможность вернуться к предыдущему режиму. Это обычная инженерная проверка нового runtime, а не повод переписывать весь проект заранее.
Проверки, записи и повторяемость результата
Автоматический HTTP-прогон проверяет девятнадцать инвариантов в восьми опытах: создание нового Kernel в classic, сохранение worker, очистку пользователя, JSON-статусы, исключение и следующий запрос, статику, PDO и перераздание ядра. Дополнительные интерфейсные тесты проверяют навигацию, ошибку загрузки, повтор, границы конфигурации и защиту от устаревшего ответа.
Verify создаёт новую проверочную запись в runtime и не меняет опубликованный artifacts/recording.json. Capture намеренно переснимает восемь опытов. После изменения Kernel или Caddyfile сохранённые SHA256 перестанут соответствовать проекту, и тесты это обнаружат. Это помогает не показывать старую демонстрацию под видом результата нового кода.
При повторе изменяются дата, идентификаторы и задержки. Сравнивать нужно смысл и инварианты, а не случайные значения. Проверка на Windows и Linux подтверждает конкретный комплект и команды; она не заменяет приёмку вашего VPS. Адаптивный просмотр мастерской также относится к отдельному уровню проверки интерфейса.
npm test
npm run verify
# Только для осознанного обновления исторической записи:
npm run capture
npm test
npm run build
Что взять в свой проект и где читать дальше
Начните с нужного вам сценария: простой PHP API, существующая панель или приложение с дорогой подготовкой. Запустите рабочий пример, изучите запросы, затем перенесите свою маршрутизацию и зависимости. Для worker составьте список request-scoped данных и проверьте, что они очищаются после всех вариантов завершения.
Полные исходники включают статическую мастерскую, PHP-приложение, записи, тесты, Dockerfile, Compose, инструкции Windows/Linux и примеры VPS. Собственный код доступен по MIT, лицензии зависимостей приложены. Публичная страница не требует удалённого запуска PHP от посетителя: все серверные опыты можно повторить в своём окружении.
Официальные материалы: classic mode, worker mode, конфигурация, Docker и production-развёртывание. Сопоставляйте документацию с версией бинарника и сохраняйте свои проверенные сценарии рядом с приложением.


