RRelayer
ホーム/運用

デプロイ(Dockerfile)#

Relayer アプリをコンテナで本番に出すための Dockerfile の組み方をまとめます。ポイントは「本番では PSX を事前コンパイルする」「ビルドと実行で同じ絶対パス」「public/ をドキュメントルートにしたフロントコントローラ」の 3 点です。

本番で押さえること#

  • PHP 8.5+composer install --no-dev --optimize-autoloader
  • APP_ENVdev 以外(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 を、ページ/レイアウトは AppRouterdirname(<root>/src/Pages)/var/cache/psx(= <root>/src/var/cache/psx)を見る。

  • public/index.phpRelayer::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 になりません。

  • 走査時の曖昧さはデプロイ時に失敗しますpageroute.php

の衝突やルートグループによる URL 衝突は、本番初回リクエストではなくこの routes:compile で検出されます。

  • PSX 事前コンパイルとは独立した別ステップです(usephp compile

.psx を、routes:compile はルート表を畳む)。両方ビルド時に実行します。--cache のような絶対パス指定は不要で、出力は常に <projectRoot>/var/cache/routes/routes.php。生成物は自動生成のスナップショットなので手で編集しません(再生成は同コマンド)。

  • 終了コード: 0 成功 / 1 src/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 コンテナ -> PHP

APP_ENVdev 以外(未設定でも可)にし、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/Caddyfile

nginx + 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 をビルドします。

最終更新: 2026-05-20
変更履歴 (4)
  • v0.19.0 の DI コンテナ事前コンパイル(container:compile)と3アーティファクトのビルド手順まとめを追加
  • サイドバー再構成: 開発カテゴリ追加に伴う order 調整
  • ルート事前コンパイル(relayer routes:compile)のセクションとDockerfile手順を追加
  • 新規作成