FrankenPHPは、Go製WebサーバーのCaddyにPHPを組み込んだアプリケーションサーバーです。Nginx+PHP-FPMの2段構成を1つのバイナリに畳み、HTTPSの自動化やHTTP/3、アプリを常駐させるワーカーモードまで扱います。2025年5月にはPHP Foundationの公式プロジェクトになりました。
本記事では、2026年9月時点の最新版v1.12.7を前提に、PHP-FPMとの実行モデルの違い、導入手順、Laravel・Symfony・WordPressでの動かし方、導入前に確認すべき制約を整理します。挙動の説明には、macOS版バイナリで実際に取得した出力を載せています。
まとめ:FrankenPHPで変わる点と導入前の確認事項
- FrankenPHPはCaddyとPHPを1つにまとめたアプリサーバーで、2025年5月15日からPHP Foundationが公式にサポートしています。
- 設定なしで起動するとクラシックモードになり、PHP-FPMの置き換えとして既存のPHPファイルをそのまま配信します。プロセスではなくスレッドでPHPを動かす点が、PHP-FPMとの根本的な違いです。
- ワーカーモードはアプリを1回だけ起動してメモリに常駐させます。速くなる代わりに、変数やstaticの値が次のリクエストへ持ち越されます。
- LaravelはOctane経由、Symfonyは7.4以降ならネイティブでワーカーモードに対応します。WordPressの公式手順はphp-serverをそのまま実行する形です。
- Windowsはv1.12.0(2026年3月6日)でネイティブ対応しました。一方、imap・newrelic・pcovはスレッド非対応のため使えません。
- 使っている拡張にスレッド非対応のものが無く、コンテナで配布している案件なら候補になります。共有レンタルサーバーの案件は対象外です。
FrankenPHPとは:Caddyに組み込んだGo製のPHPアプリサーバー
FrankenPHPはKévin Dunglas氏が開発したPHPのアプリケーションサーバーです。PHPインタプリタをGoのプログラムから呼び出し、Caddyのモジュールとして動かします。HTTPの受付からPHPの実行までを1プロセスで完結させるため、Webサーバーと別にPHP-FPMを立てる必要がありません。
2025年5月にPHP Foundationの公式プロジェクトへ移管
PHP Foundationは2025年5月15日のブログ記事「FrankenPHP Is Now Officially Supported by The PHP Foundation」で、FrankenPHPを公式にサポートすると発表しました。ソースコードはPHPプロジェクトのGitHub組織へ移され、現在のリポジトリは github.com/php/frankenphp です。発表では、ガバナンスは変えず、Kévin Dunglas・Robert Landers・Alexander Stecherの3名が引き続き開発を担うと明記されています。
個人プロジェクトのまま消えるリスクを気にして採用を見送っていた場合、この移管が判断材料になります。ただしPHP本体と同じリリース体制に入ったわけではなく、FrankenPHPのリリースは独自の番号で出ています。
スタンドアロンバイナリに含まれるPHP 8.5とCaddy
公式が配布するバイナリには、PHP本体とCaddyがあらかじめ組み込まれています。macOS(x86_64)版のv1.12.7をダウンロードして確認すると、次の組み合わせでした。ファイルサイズは186,976,736バイト(約178MiB)です。
$ ./frankenphp-mac-x86_64 version
FrankenPHP v1.12.7 PHP 8.5.10 Caddy v2.11.4 ...
サーバーにPHPを別途インストールしなくても、このファイル1つでPHPアプリを配信できます。Linux向けのmusl版は完全静的リンクで、依存パッケージなしで動きます。GNU版はglibcに依存し、Alpineなどのmusl環境ではそのまま動きません。さらにアプリのソースコードまでバイナリへ埋め込み、アプリを1ファイルで配布する機能もあります。
PHP-FPMとの違い:プロセス方式とスレッド方式
PHP-FPMはリクエストをPHPのワーカープロセスへ振り分け、プロセス単位で実行を分けます。FrankenPHPは1つのプロセスの中にPHPのスレッドを複数立て、そこでリクエストを処理します。そのため、スレッドセーフ版(ZTS)でビルドしたPHPが必要になります。
| 項目 | Nginx+PHP-FPM | FrankenPHP(クラシック) | FrankenPHP(ワーカー) |
|---|---|---|---|
| 構成 | Nginxと常駐FPMの2つ | バイナリ1つ | バイナリ1つ |
| PHPの実行単位 | プロセス | スレッド | スレッド |
| アプリの起動 | リクエストごと | リクエストごと | ワーカー起動時の1回 |
| リクエスト間の状態 | 残らない | 残らない | 残る |
| HTTPS自動化・HTTP/3 | 別途設定 | 対応 | 対応 |
| 既存コードの改修 | 不要 | 不要 | 状態の後始末が必要 |
クラシックモードによるPHP-FPMの置き換え
何も設定せずに起動すると、FrankenPHPはクラシックモードで動きます。公式ドキュメントはこのモードを、PHP-FPMやApacheのmod_phpをそのまま置き換えられるものと位置づけています。PHPファイルはリクエストのたびに最初から実行されるため、既存コードは書き換え不要です。
リクエストのたびにカウンタを1増やすだけのスクリプトで確かめると、値は毎回1に戻りました。一方、プロセスIDは3回とも同じです。PHP-FPMでも同じワーカープロセスが複数のリクエストを処理します。PIDが同じという結果だけでは、プロセス方式とスレッド方式の違いは判別できません。
$ frankenphp php-server --root app --listen 127.0.0.1:8081
$ curl http://127.0.0.1:8081/index.php # 3回実行
classic hits=1 pid=77129 sapi=frankenphp
classic hits=1 pid=77129 sapi=frankenphp
classic hits=1 pid=77129 sapi=frankenphp
スレッド数の既定値はCPUコア数の2倍です。公式は既定値のまま使うのではなく、num_threads×memory_limitが搭載メモリを下回るように調整するよう勧めています。負荷に応じてスレッドを増やす max_threads は、PHP-FPMの pm.max_children に相当する設定です。
ワーカーモードのアプリ常駐と状態の持ち越し
ワーカーモードでは、各ワーカーが起動時にそれぞれアプリを1回読み込み、frankenphp_handle_request() でリクエストを待ち続けます。フレームワークの起動処理を毎回やり直さないため速くなりますが、スクリプト内の変数はリクエストをまたいで残ります。
公式サンプルと同じ形のワーカースクリプトで、リクエスト数とキャッシュ配列の要素数を出力させました。ワーカーを1つにし、ワーカースクリプト内で環境変数 MAX_REQUESTS=3 を読み取って、3リクエスト後に処理ループを終了する設定です。MAX_REQUESTSは、この読み取り処理を実装したスクリプトでのみ有効です。
<?php
// app/worker.php
$hits = 0;
$cache = [];
$handler = static function () use (&$hits, &$cache) {
$hits++;
$cache[] = str_repeat('x', 1024);
echo "worker hits=", $hits, " cache=", count($cache), " sapi=", PHP_SAPI, "\n";
};
$max = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($i = 0; !$max || $i < $max; ++$i) {
$keep = \frankenphp_handle_request($handler);
gc_collect_cycles();
if (!$keep) break;
}
$ MAX_REQUESTS=3 frankenphp php-server --root app \
--listen 127.0.0.1:8083 --worker app/worker.php,1
$ curl http://127.0.0.1:8083/worker.php # 5回実行
worker hits=1 cache=1 sapi=frankenphp
worker hits=2 cache=2 sapi=frankenphp
worker hits=3 cache=3 sapi=frankenphp
worker hits=1 cache=1 sapi=frankenphp
worker hits=2 cache=2 sapi=frankenphp
1〜3回目は値が積み上がり、4回目で再起動されて1に戻りました。配列に追記し続けるコードは、再起動されるまでメモリを解放しません。PHPはもともと常駐を前提に作られていないため、ライブラリ側のリークに備えてリクエスト数での再起動を残しておくのが安全です。同じサーバーでも index.php へのリクエストはクラシックモードで処理され、ワーカーの対象はワーカースクリプトだけでした。
Swoole・RoadRunnerとの位置づけ
PHPを常駐させる手段には、FrankenPHPのほかにSwooleとRoadRunnerがあります。SwooleはpeclでインストールするPHP拡張で、コルーチンによる非同期処理まで扱えます。RoadRunnerはGo製のサーバーがPHPのワーカープロセスを外から管理する方式です。FrankenPHPはPHP拡張を足さずに導入でき、常駐させないクラシックモードから始めてワーカーモードへ段階的に移れる点が特徴です。非同期処理を書きたいなら、PHPの非同期処理とFibers・ReactPHP・AMPHP・Swooleの違いで整理しているSwooleの方が向いています。
インストールと起動の手順
インストールスクリプト・Homebrew・Dockerイメージの選び方
LinuxとmacOSではインストールスクリプト、WindowsではPowerShellのスクリプトが用意されています。Homebrew(macOS・Linux)とDockerイメージでも入手できます。
# Linux / macOS
curl https://frankenphp.dev/install.sh | sh
# Windows(PowerShell)
irm https://frankenphp.dev/install.ps1 | iex
# Homebrew
brew install dunglas/frankenphp/frankenphp
# Docker(カレントディレクトリを公開ディレクトリとして配信)
docker run -v .:/app/public -p 80:80 -p 443:443 -p 443:443/udp dunglas/frankenphp
Dockerイメージのタグは dunglas/frankenphp:<FrankenPHPの版>-php<PHPの版>-<OS> の形で、OSは trixie・bookworm・alpine から選びます。本番ではDebian系を選んでください。AlpineはmuslというCライブラリを使っており、公式ドキュメントはZTS版のPHPがmuslで遅くなりやすいとして、本番ではglibc版を推奨しています。拡張の追加はDockerfile内で install-php-extensions コマンドを使います。
php-serverによる既存PHPアプリの配信
いちばん手早いのは、公開ディレクトリで php-server サブコマンドを実行する方法です。設定ファイルは要りません。
# カレントディレクトリを配信
frankenphp php-server
# ドキュメントルートと待ち受けアドレスを指定
frankenphp php-server --root public --listen 127.0.0.1:8080
# ドメインを指定するとHTTPSで待ち受け、証明書を自動取得
frankenphp php-server --domain example.com
# ワーカーモードで起動し、PHPファイルの変更で再起動
frankenphp php-server --worker public/index.php --watch
このphp-serverの既定構成では、HTTP/2とHTTP/3を有効にするためにTLSを使います。--listen でHTTPのまま起動したときのログには「HTTP/2 skipped because it requires TLS」と「HTTP/3 skipped because it requires TLS」が出ていました。HTTP/3を試すなら --domain を付けてHTTPSで起動してください。PHPスクリプトをコマンドラインで実行するときは frankenphp php-cli script.php を使います。
Caddyfileのスレッド数とワーカー設定
本番では frankenphp run でCaddyfileを読み込んで起動します。スレッド数やワーカーは、グローバルオプションの frankenphp ブロックで指定します。
{
frankenphp {
num_threads 8
max_threads auto
max_wait_time 10s
worker {
file /app/public/index.php
num 4
watch /app/src
}
}
}
example.com {
root /app/public
encode zstd br gzip
php_server
}
max_wait_time は、空きスレッドを待つリクエストをタイムアウトさせる設定です。既定では無効で、スレッドが埋まるとリクエストは無期限に待たされます。遅いAPIだけを別のスレッドプールに分ける設定も公式ドキュメントに載っているので、外部API待ちが長いエンドポイントがある場合は併せて検討してください。
FrankenPHPの前段にNginxやロードバランサーを置く場合は、Caddyfileで trusted_proxies を、フレームワーク側で信頼するプロキシを設定します。どちらかが欠けるとX-Forwarded-Forなどのヘッダーが無視され、HTTPSの判定やクライアントIPが誤ります。
フレームワーク別の動かし方
Laravel:Octane経由のワーカーモード起動
LaravelをクラシックモードでFrankenPHPに載せる公式手順では、プロジェクト直下のCaddyfileで root を public/ に向け、php_server に try_files {path} index.php を指定してから frankenphp run で起動します。ワーカーモードを使う場合は、Laravel Octaneを経由するのが公式の手順です。
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:frankenphp --workers=4 --max-requests=500
octane:frankenphp の既定ポートは8000、--max-requests の既定値は500です。--https を付けるとHTTPS・HTTP/2・HTTP/3と証明書の自動更新が有効になります。Octaneではサービスコンテナへの注入やstaticプロパティへの追記がリクエストをまたいで残るため、既存アプリを載せる前に点検が必要です。点検箇所とワーカー数の決め方はLaravel Octaneのワーカー常駐の仕組みと状態リークで詳しく扱っています。
Laravelプロジェクトでは、composer.json のスクリプトにある @php artisan package:discover が失敗することがあります。ComposerがFrankenPHPのバイナリの呼び方を知らず、-d オプションも渡されるためです。PHPを別途入れていない環境では、-d を取り除いて frankenphp php-cli を呼ぶラッパースクリプトを用意し、PHP_BINARY 環境変数でそのパスを指定します。
Symfony:7.4以降はネイティブ対応、それ以前はRuntimeパッケージ
Symfony 7.4以降はFrankenPHPのワーカーモードにネイティブ対応しています。7.3以前では runtime/frankenphp-symfony パッケージを入れ、APP_RUNTIME 環境変数でFrankenPHP用のRuntimeを指定します。
composer require runtime/frankenphp-symfony
docker run \
-e FRANKENPHP_CONFIG="worker ./public/index.php" \
-e APP_RUNTIME=Runtime\\FrankenPhpSymfony\\Runtime \
-v $PWD:/app -p 80:80 -p 443:443 -p 443:443/udp \
dunglas/frankenphp
リクエスト固有の状態を持つサービスは ResetInterface を実装してリセットさせます。公式ドキュメントでは、リセット漏れや可変のstaticを静的解析で洗い出すツールとしてIgor PHPが紹介されています。
WordPress:展開先ディレクトリからのphp-server起動
WordPressの公式手順は単純で、ダウンロードしたZIPを展開したディレクトリで frankenphp php-server を実行し、/wp-admin/ からインストールを進めるだけです。本番向けには、ドメイン・php_server・圧縮・ログの4行を書いたCaddyfileを frankenphp run で読み込む構成が示されています。
example.com
php_server
encode zstd br gzip
log
WordPressのドキュメントにワーカーモードの手順は載っていません。プラグインが書き換えるグローバル変数を次のリクエストへ持ち越すと不具合の原因になるため、WordPressではクラシックモードで使い、HTTPS自動化やHTTP/3、Zstandard圧縮の恩恵を受ける使い方が現実的です。WordPress本体の最新版についてはWordPress 7.0「Armstrong」の新機能・変更点と安全なアップデート手順を参照してください。
導入前に確認する制約と既知の問題
スレッド非対応の拡張(imap・newrelic・pcov)とimagick
FrankenPHP公式ドキュメントの既知の問題(Known issues)では、imap・newrelic・pcovの3つがスレッドセーフでないため非対応とされています。imapには webklex/php-imap などの代替があり、pcovはFrankenPHPの外でCLIからテストを走らせてカバレッジを取るよう案内されています。New Relicの代替は示されていません。APMにNew Relicを使っている案件は、この時点で採用を見送る理由になります。
不具合が報告されている拡張もあります。imagickはImageMagickのOpenMPスレッドがFrankenPHPのスレッドと衝突してクラッシュすることがあり、ImageMagick側のスレッド数を1に制限するか、OpenMPを無効にしたImageMagickを使います。公式静的バイナリと公式apt・apk・rpmパッケージではOpenMPが無効化済みで、公式資料が影響対象として挙げるのはDockerとHomebrewです。DatadogのプロファイリングとBlackfireも、それぞれ不安定・ベータ扱いです。
公式バイナリに入っていない拡張を足す方法は、バイナリの種類で変わります。muslの完全静的バイナリは拡張を後から読み込めず、ビルド時に組み込むしかありません。glibc版とmacOS版は読み込めますが、拡張もZTS版でビルドする必要があります。パッケージマネージャーの多くはZTS版の拡張を配布していないため、XdebugのPHPデバッグの使い方・インストール手順のような拡張も自前でビルドすることになります。拡張が多い案件ではDockerイメージの install-php-extensions を使う方が手間がかかりません。
Windowsのネイティブ対応とv1.12.0以降の導入
2026年2月24日公開のv1.11.3までは、リリースに含まれるバイナリはLinux版とmacOS版だけで、Windows版はありませんでした。2026年3月6日公開のv1.12.0でWindowsにネイティブ対応し、リリースノートではワーカーモードとホットリロードもWindowsで使えると案内されています。Windows向けのアーカイブには、PHP公式のWindows用バイナリが同梱されています。
Windowsサービスとして登録した場合、サービスは設定の再読み込みができません。設定変更は、設定ファイルのあるディレクトリで .\frankenphp.exe reload --config Caddyfile を実行すると、サービスを再起動せずに反映できます。
AlpineイメージとGLOB_BRACE
完全静的バイナリとAlpineベースのイメージは、バイナリを小さくするためにmusl libcを使っています。そのため glob() の GLOB_BRACE フラグが使えません。設定ファイルの読み込みにGLOB_BRACEを使うライブラリで問題が出たら、GNU版の静的バイナリかDebianベースのイメージに切り替えます。
FrankenPHPを採用する案件と見送る案件の線引き
採用を勧めるのは、Dockerでアプリを配布しており、Nginxの設定とPHP-FPMのチューニングを二重に抱えているLaravel・Symfonyの案件です。まずクラシックモードで置き換え、構成が1つに減ることとHTTPSの自動化を確かめてから、ワーカーモードへ進む順番にすると、状態の持ち越しによる不具合を切り分けやすくなります。
見送るべきなのは次の案件です。
- New Relicでアプリを監視しており、APMを変える予定が無い
- imap拡張に依存したメール処理があり、代替ライブラリへ移す工数を取れない
- 共有レンタルサーバーで運用しており、常駐プロセスやバイナリを置けない
- 速度の不満がDBクエリや外部APIの待ち時間から来ている
最後の条件は見落とされがちです。ワーカーモードで短くなるのはフレームワークの起動時間であり、遅いSQLは遅いままです。導入前にリクエストの処理時間の内訳を計測し、起動処理が大きな割合を占めていることを確かめてください。起動時間の短縮だけが目的なら、OPcacheの設定と確認方法の見直しとプリロードで足りる場合もあります。
運用に入ったら、Caddyのメトリクスを有効にして 通常リクエストの待ち行列は frankenphp_queue_depth、ワーカーの待ち行列と処理状況は frankenphp_worker_queue_depth と frankenphp_busy_workers で監視します。frankenphp_busy_threads には待機中の常駐ワーカーが占有するスレッドも含まれます。キューが継続的に積み上がる場合は、スレッド数だけでなくDB・外部APIの遅延や流入量も調べます。frankenphp_worker_crashes が増えているならワーカースクリプトが異常終了しています。
よくある質問
FrankenPHPとPHP-FPMの違いは何ですか?
PHP-FPMはプロセス単位でPHPを実行し、Nginxなどと組み合わせて使います。FrankenPHPはCaddyと一体になったバイナリで、1つのプロセス内のスレッドでPHPを実行します。HTTPSの自動化やHTTP/3に対応し、アプリを常駐させるワーカーモードも使えます。
WordPressはFrankenPHPで動きますか?
動きます。WordPressを展開したディレクトリで frankenphp php-server を実行すると、そのままインストール画面に進めます。公式ドキュメントのWordPress手順はクラシックモードでの起動で、ワーカーモードの手順は載っていません。
FrankenPHPはWindowsで使えますか?
使えます。2026年3月6日公開のv1.12.0からWindowsにネイティブ対応し、PowerShellのインストールスクリプトかリリースページのZIPで導入できます。ワーカーモードとホットリロードもWindowsで動作します。
FrankenPHPの前にNginxを置くことはできますか?
置けます。その場合はCaddyfileのグローバルオプションで trusted_proxies を設定し、LaravelやSymfony側でも信頼するプロキシを設定してください。片方だけではX-Forwarded-Forなどが無視され、クライアントIPやHTTPSの判定が誤ります。
FrankenPHPの稼働状況はどう監視しますか?
Caddyのメトリクスを有効にすると、FrankenPHPのスレッド数、処理中のスレッド数、待ち行列の長さ、ワーカーのクラッシュ回数や再起動回数がPrometheus形式で出力されます。キューの長さとクラッシュ回数を監視すると、スレッド不足とワーカーの異常を区別できます。