デプロイ(Dockerfile)#
Relayer アプリをコンテナで本番に出すための Dockerfile の組み方をまとめます。ポイントは「本番では PSX を事前コンパイルする」「ビルドと実行で同じ絶対パス」「public/ をドキュメントルートにしたフロントコントローラ」の 3 点です。
本番で押さえること#
- PHP 8.5+。
composer install --no-dev --optimize-autoloader。 APP_ENVはdev以外(dev だとリクエスト毎にオンザフライ
コンパイル+プロファイラが走る。本番は無効化)。
- PSX を事前コンパイル(後述)。dev のオンザフライは本番では使わない。
- ルートを事前コンパイル(後述)。既定はリクエスト毎に
src/Pages/
を走査するので、デプロイ時に 1 ファイルへ畳む。
- DI コンテナを事前コンパイル(後述)。既定は毎リクエストの
ContainerBuilder::compile() が回避可能な最大コストなので、PHP クラスにダンプして本番は require だけにする。
public/usephp.jsを配置(composer installの post-install で自動。
手動なら vendor/bin/usephp publish)。
StorageType::Snapshotを使うならUSEPHP_SNAPSHOT_SECRETを設定。- ドキュメントルートは
public/、実ファイル以外はpublic/index.php
へ流すフロントコントローラ。
PSX 事前コンパイルの要点(ここが肝)#
APP_ENV が dev 以外だと .psx は自動コンパイルされないため、ビルド時に コンポーネントとページ/レイアウトの両方をコンパイルします(例はランタイムの WORKDIR が /app の場合。/var/www/html などでも考え方は同じ)。
# コンポーネント(PascalCase)→ <projectRoot>/var/cache/psx(manifest 生成)
php vendor/bin/usephp compile src/Components --cache="$PWD/var/cache/psx"
# ページ/レイアウト → <projectRoot>/src/var/cache/psx
# AppRouter は dirname(src/Pages)/var/cache/psx を見る = src/var/cache/psx
php vendor/bin/usephp compile src/Pages --cache="$PWD/src/var/cache/psx"なぜこうなるか:
- キャッシュのファイル名は
sha1(realpath(ソース))。だから
ビルド時とランタイムで同じ絶対パスである必要があります(単一ステージで、ランタイムと同じ WORKDIR のままコンパイルするのが安全)。
--cacheは絶対パスで渡す。相対だとコンポーネント manifest に相対
パスが入り、実行時 cwd と食い違って "Compiled PSX file not found" になる。
- キャッシュは 2 か所に分かれる。コンポーネントは
PsxComponentRegistrar が <root>/var/cache/psx を、ページ/レイアウトは AppRouter が dirname(<root>/src/Pages)/var/cache/psx(= <root>/src/var/cache/psx)を見る。
public/index.phpはRelayer::boot(\dirname(__DIR__), …)のように
正規化した絶対 projectRoot を渡す(__DIR__ . '/..' の /.. を残さない)。キャッシュキーがブレない。
- ページが
<Shell>などのコンポーネントを使う場合、そのページ先頭で
// @psx-runtime App\Components\Shell を宣言する。バッチコンパイラに「実行時 manifest で解決する」と伝えるためで、無いと usephp compile src/Pages が exit 1(ファイル未生成)になる。
実行時にファイル書き込みが無い設計(外部 DB / 静的 ETag / プロファイラ無効)なら、コンテナ FS は読み取り専用で問題ありません。セッションや一時ファイルは /tmp に向けます。
ルートの事前コンパイル#
既定ではルーターはリクエストごとに src/Pages/ を走査します。デプロイ時に vendor/bin/relayer routes:compile を実行すると、可読でポータブルなルートのスナップショットを var/cache/routes/routes.php に 1 つ書き出し、本番はツリーを歩く代わりにこの OPcache 済みファイルだけを読みます。
php vendor/bin/relayer routes:compile- 存在するかどうかだけがゲートです。ファイルがあればそれを使い、
無ければライブ走査に戻ります。dev はコンパイルしないので常に最新のツリーを反映し、stale になりません。
- 走査時の曖昧さはデプロイ時に失敗します。
pageとroute.php
の衝突やルートグループによる URL 衝突は、本番初回リクエストではなくこの routes:compile で検出されます。
- PSX 事前コンパイルとは独立した別ステップです(
usephp compileは
.psx を、routes:compile はルート表を畳む)。両方ビルド時に実行します。--cache のような絶対パス指定は不要で、出力は常に <projectRoot>/var/cache/routes/routes.php。生成物は自動生成のスナップショットなので手で編集しません(再生成は同コマンド)。
- 終了コード:
0成功 /1src/Pagesが無い・走査不能・書き込み
失敗。
詳細は CLI コマンド を参照。
DI コンテナの事前コンパイル#
既定では Relayer::boot() はリクエスト毎に Symfony の ContainerBuilder をビルドして compile() まで回します — 通常これが回避可能なリクエストコストの最大要因です。デプロイ時に vendor/bin/relayer container:compile を実行すると、素の PHP クラスを var/cache/container/CompiledContainer.php にダンプし、本番は ContainerBuilder を作らずそれを require します。
php vendor/bin/relayer container:compile- 存在するかどうかだけがゲート。ファイルがあれば本番はそれを
使い、無ければライブビルドに戻ります。dev はコンパイルしないので常に最新のサービス設定を反映し stale になりません。
- 不正なサービス定義はデプロイ時に失敗します(
config/services.yaml
/ App\AppConfigurator の構成エラー)。本番初回リクエストではなくこの container:compile で検出されるので、配布物が壊れません。
- PSX 事前コンパイル・
routes:compileとは独立した別ステップです
(3 つそれぞれ別のキャッシュを生成)。出力は常に上記パスで、自動生成・手で編集しません。サービス構成を変えたら再生成します(同コマンド)。
- 終了コード:
0成功 /1ビルド / ダンプ / 書き込み失敗。
ビルド手順まとめ#
composer install 後に 3 つのアーティファクトを生成して、OPcache 済み・走査ゼロのイメージを作ります:
composer install --no-dev --classmap-authoritative
vendor/bin/usephp compile src/Components --cache="$PWD/var/cache/psx"
vendor/bin/usephp compile src/Pages --cache="$PWD/src/var/cache/psx"
vendor/bin/relayer routes:compile # ルート表 -> PHP
vendor/bin/relayer container:compile # DI コンテナ -> PHPAPP_ENV を dev 以外(未設定でも可)にし、OPcache の validate_timestamps=0 で起動します。各ステップは有無ゲートなので、アーティファクトが無くてもライブ経路に縮退するだけで壊れません。
サーバー構成#
FrankenPHP(単一バイナリ・推奨)#
FrankenPHP は Caddy に PHP を埋め込んだ単一バイナリ。nginx も php-fpm も不要で、php_server ディレクティブがフロントコントローラ(public/ を root に、実ファイル以外は index.php)を最初から行います。PHP に環境変数もそのまま渡る(clear_env 問題なし)。
- 待受アドレスは環境変数
SERVER_NAME。TLS を上流(Fly.io / ロード
バランサ)で終端するなら SERVER_NAME=":8080" のようにポートのみ 指定(= 平 HTTP、Caddy の自動 HTTPS を使わない)。$PORT 注入型の PaaS では起動時に SERVER_NAME=":$PORT" を組み立てる。
- Caddy はデータ/設定を XDG ディレクトリに書くため、読み取り専用 FS では
XDG_DATA_HOME / XDG_CONFIG_HOME を /tmp 配下に向ける。
nginx + php-fpm#
public/ を root にし、実ファイル以外を index.php に流します。 php-fpm は clear_env = no でコンテナの環境変数を PHP に渡します(環境変数 参照)。
server {
listen __PORT__;
root /var/www/html/public;
location / { try_files $uri /index.php$is_args$args; }
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}; php-fpm.d/zz-app.conf
[www]
clear_env = no ; APP_ENV / DB 認証情報などを PHP に渡す
catch_workers_output = yes ; PHP のエラーをコンテナログ(stderr)へ$PORT を注入する PaaS では設定が環境変数を直接読めないので、起動スクリプトで置換してから両プロセスを起動します。
#!/bin/sh
set -e
: "${PORT:=8080}"
sed -i "s/__PORT__/${PORT}/g" /etc/nginx/nginx.conf
php-fpm --nodaemonize &
exec nginx -g 'daemon off;'参考 Dockerfile#
FrankenPHP#
FROM dunglas/frankenphp:php8.5
# FrankenPHP イメージは install-php-extensions を同梱
RUN install-php-extensions mbstring opcache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
# 本番 ini(php.ini-production をベースに上書きを重ねる)
RUN cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
ENV APP_ENV=production \
COMPOSER_ALLOW_SUPERUSER=1 \
SERVER_NAME=":8080" \
XDG_DATA_HOME=/tmp/caddy \
XDG_CONFIG_HOME=/tmp/caddy
# FrankenPHP の既定ドキュメントルートは /app/public(php_server が
# フロントコントローラを担う)。ランタイムと同じ /app でコンパイルする。
WORKDIR /app
COPY . .
RUN set -eux; \
composer install --no-dev --optimize-autoloader --no-interaction; \
php vendor/bin/usephp compile src/Components --cache=/app/var/cache/psx; \
php vendor/bin/usephp compile src/Pages --cache=/app/src/var/cache/psx; \
php vendor/bin/relayer routes:compile; \
php vendor/bin/relayer container:compile
COPY docker/php.ini "$PHP_INI_DIR/conf.d/zz-app.ini"
EXPOSE 8080
# 既定 CMD(frankenphp run)が SERVER_NAME の Caddyfile で起動する$PORT を注入する PaaS なら SERVER_NAME を起動時に組み立てるエントリポイントを足します。
#!/bin/sh
set -e
export SERVER_NAME=":${PORT:-8080}"
exec frankenphp run --config /etc/frankenphp/Caddyfilenginx + php-fpm#
FROM php:8.5-fpm
# php:8.5-fpm は mbstring/opcache/ctype/curl/openssl/json/tokenizer 同梱
RUN set -eux; apt-get update; \
apt-get install -y --no-install-recommends nginx unzip; \
rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
ENV APP_ENV=production COMPOSER_ALLOW_SUPERUSER=1 PORT=8080
WORKDIR /var/www/html
COPY . .
RUN set -eux; \
composer install --no-dev --optimize-autoloader --no-interaction; \
php vendor/bin/usephp compile src/Components --cache=/var/www/html/var/cache/psx; \
php vendor/bin/usephp compile src/Pages --cache=/var/www/html/src/var/cache/psx; \
php vendor/bin/relayer routes:compile; \
php vendor/bin/relayer container:compile; \
chown -R www-data:www-data /var/www/html
COPY docker/nginx.conf /etc/nginx/nginx.conf
COPY docker/php-fpm.conf /usr/local/etc/php-fpm.d/zz-app.conf
COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]php:8.5-apache でも可(mod_php + FallbackResource /index.php でフロントコントローラにする)。
確認#
composer install --no-dev後、vendor/bin/relayer routesで
ルートが想定どおり出るか(ルーティングとページ)。
- ビルドしたイメージをローカル起動し、
/・動的セグメント・route.php・
404 が正しく返るか、ログに "Compiled PSX not found" が出ないか。
このサイトの場合#
本サイトは FrankenPHP イメージ(上記の形)を Fly.io にデプロイし、記事ストアは Turso(libSQL/HTTP、未設定ならローカル SQLite フォールバック)です。ストア接続は環境変数で自動切替で、サーバーは読むだけ・記事更新はローカル CLI bin/docs(環境変数 / データベース 参照)。CI は GitHub Actions から flyctl deploy --remote-only で Fly のリモートビルダーがこの Dockerfile をビルドします。
変更履歴 (4)
- v0.19.0 の DI コンテナ事前コンパイル(container:compile)と3アーティファクトのビルド手順まとめを追加
- サイドバー再構成: 開発カテゴリ追加に伴う order 調整
- ルート事前コンパイル(relayer routes:compile)のセクションとDockerfile手順を追加
- 新規作成