# 2026年の Symfony と Docker:開発環境と本番デプロイ > 2026年版の完全な Symfony Docker ワークフロー。FrankenPHP 開発環境、マルチステージ本番イメージ、ワーカーモード、暗号化シークレット、イメージ最適化、マイグレーションを伴うゼロダウンタイムデプロイまでを解説します。 - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- この Symfony Docker チュートリアルでは、コンテナワークフロー全体を構築します。再現性の高いローカル開発環境と堅牢な本番イメージの両方を、FrankenPHP と Symfony 7.4 LTS を中心に組み立てます。Symfony をコンテナで動かせば、ノート PC とサーバーのあいだで生じる古典的な環境差異が解消され、各デプロイは壊れやすい手動サーバーコマンドの連続実行ではなく、単一の不変アーティファクトを配布するだけの作業になります。 > **2026年の Symfony Docker スタック** > > 公式の Symfony Docker テンプレートは、従来の Nginx と PHP-FPM の組み合わせに代わり、ランタイムとして [FrankenPHP](https://frankenphp.dev/) を採用するようになりました。単一のバイナリが HTTP/2 と HTTP/3 を配信し、リクエストごとにカーネルを再構築するのではなく起動状態のまま保持することで、Symfony を永続的なワーカーモードで実行します。 ## Symfony 向け Docker Compose 開発環境 良い開発環境は、ホストマシンに手を加えることなく、すべての貢献者に同じ PHP バージョン、同じ拡張、同じデータベースを提供します。Docker Compose はそのスタックを宣言的に記述します。以下の例では、ローカルの `Dockerfile` からビルドした FrankenPHP コンテナと PostgreSQL 17 サービスを組み合わせ、既定の Compose ネットワーク上で接続しています。 ```yaml # compose.yaml services: php: build: context: . target: frankenphp_dev ports: - "80:80" - "443:443" volumes: - ./:/app # bind mount: edits on the host appear instantly - caddy_data:/data # persist TLS certificates across restarts environment: DATABASE_URL: "postgresql://app:app@database:5432/app?serverVersion=17" depends_on: - database database: image: postgres:17-alpine environment: POSTGRES_DB: app POSTGRES_USER: app POSTGRES_PASSWORD: app volumes: - db_data:/var/lib/postgresql/data volumes: caddy_data: db_data: ``` `php` サービスのバインドマウントはプロジェクトルートをコンテナ内へマッピングするため、ファイルの変更はイメージを再ビルドしなくても反映されます。FrankenPHP は初回起動時にローカルの TLS 証明書を生成します。ポート 443 を公開し、`caddy_data` を名前付きボリュームにしているのはこのためで、証明書は `docker compose down` をまたいでも保持されます。DSN 内の `serverVersion=17` というヒントによって、Doctrine は接続のたびに実行されるバージョン検出クエリを省略できます。 スタックが起動したあとは、すべての Symfony コマンドがホストではなく `php` コンテナ内で実行されるため、正しい PHP バージョンと拡張が保証されます。データベースの作成、マイグレーションの実行、シェルの起動はいずれも `docker compose exec` を通じて同じパターンで行います。 > **コンテナ内でコンソールコマンドを実行する** > > Symfony や Composer の呼び出しには `docker compose exec php` を前置し、コンテナの PHP ランタイムに対して実行します。たとえば `docker compose exec php bin/console make:entity` や `docker compose exec php composer require symfony/uid` のようになります。ホスト側に PHP をインストールする必要は一切ありません。 ## Symfony 本番向けマルチステージ Dockerfile [マルチステージ Dockerfile](https://docs.docker.com/build/building/multi-stage/) は共通のベースを共有したうえで、開発ターゲットと本番ターゲットに分岐します。本番ステージはランタイム依存のみをインストールし、Composer の開発用パッケージを除外し、ビルド時にキャッシュをウォームアップするため、稼働するコンテナは即座に起動します。 ```dockerfile # Dockerfile FROM dunglas/frankenphp:1-php8.4 AS base WORKDIR /app # Install the PHP extensions Symfony relies on RUN install-php-extensions \ intl \ opcache \ pdo_pgsql \ zip COPY --from=composer/composer:2-bin /composer /usr/bin/composer # --- Development target --- FROM base AS frankenphp_dev ENV APP_ENV=dev RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" # --- Production target --- FROM base AS frankenphp_prod ENV APP_ENV=prod ENV FRANKENPHP_CONFIG="worker ./public/index.php" ENV APP_RUNTIME="Runtime\FrankenPhpSymfony\Runtime" RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" # Layer caching: copy manifests first, install, then copy the source COPY composer.json composer.lock symfony.lock ./ RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist COPY . . RUN composer dump-autoload --no-dev --classmap-authoritative \ && composer dump-env prod \ && bin/console cache:warmup ``` 残りのソースより先に `composer.json` とロックファイルをコピーすることが鍵となる最適化です。Docker は依存インストールのレイヤーをキャッシュし、コード編集のたびではなくマニフェストが変わったときだけ再実行します。`--classmap-authoritative` フラグは Composer にクラスマップが完全であることを伝えるため、オートローダーは実行時にファイルシステムへフォールバックすることがありません。ビルド中にキャッシュをウォームアップしておくことで、最初の本番リクエストは完全にコンパイル済みのコンテナに届きます。 ## FrankenPHP ワーカーモードと Symfony ランタイム 従来の PHP-FPM は Symfony カーネルを起動し、1 リクエストを処理して、すべてを破棄します。ワーカーモードはカーネルと依存性注入コンテナをリクエストをまたいで生かし続けます。レイテンシ削減の大半はここから生まれます。`runtime/frankenphp-symfony` パッケージは、Symfony Runtime コンポーネントと標準の `public/index.php` ブートストラップを通じてこれを実現します。 ```php // public/index.php use App\Kernel; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; return function (array $context) { return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']); }; ``` Dockerfile で設定した `FRANKENPHP_CONFIG="worker ./public/index.php"` の行がこのループを有効化します。コンテナが再利用されるため、リクエスト固有の状態を保持するサービスはリクエスト間で自身をリセットしなければなりません。Symfony はフレームワークのサービスを自動的に処理しますが、状態を持つカスタムサービスは `Symfony\Contracts\Service\ResetInterface` を実装し、`kernel.reset` でタグ付けして、蓄積された状態が次のリクエストへ漏れ出さないようにするべきです。これは長時間稼働する [Messenger ワーカー](/blog/symfony/symfony-messenger-queues-workers-async-architecture) に求められる規律と同じで、その見返りとして同一ハードウェアで PHP-FPM の数倍のリクエストスループットが得られます。 ローカル開発では、FrankenPHP を `--watch` フラグ付きで実行するとファイル変更時にワーカーが自動的に再起動するため、ワーカーモードが編集とリフレッシュのループの妨げになることはありません。 ## Symfony における FrankenPHP と PHP-FPM の比較 FrankenPHP を選ぶか、従来の Nginx と PHP-FPM のスタックを選ぶかは、Dockerfile とランタイムの挙動の両方を左右します。以下の表は、Symfony の本番構成における実務上の違いをまとめたものです。 | 観点 | FrankenPHP ワーカーモード | Nginx + PHP-FPM | |--------|------------------------|-----------------| | コンテナ | 1 つ | 2 つ(Web サーバー + PHP) | | カーネル起動 | ワーカーごとに 1 回 | リクエストごとに 1 回 | | HTTP/3 | 標準搭載 | 追加設定が必要 | | TLS | 自動(Caddy) | 手動での証明書設定 | | 状態の扱い | リクエスト間でリセットが必要 | リクエストごとに分離 | | コールドスタートのコスト | 起動時に一度だけ発生 | リクエストごとに発生 | ワーカーモードは、コストの高いカーネル起動とコンテナコンパイルのステップを数千リクエストにわたって償却するため、スループットで勝ります。PHP-FPM が依然として有効なのは、グローバル変数や、プロセスがリクエストごとに新規である前提のサードパーティライブラリに依存するアプリケーションの場合です。それらは再利用されるワーカーのもとでは破綻するためです。 > **ワーカーモードでの状態漏れ** > > リクエストデータをプライベートプロパティにキャッシュするサービスは、リセットしない限り、あるリクエストのデータを次のリクエストへ渡してしまいます。本番でワーカーモードに切り替える前に、シングルトン、イベントサブスクライバー、そしてセキュリティトークンや現在のリクエストを保持するあらゆるものを監査してください。 ## 本番環境での環境変数とシークレットの管理 Symfony は環境変数から設定を読み込みます。本番でそれらを供給する健全な方法は 2 つあります。1 つ目はオーケストレーターや Compose ファイルを通じてプレーンな変数を注入する方法です。2 つ目は Symfony の暗号化シークレットボルトで、機微な値を暗号文としてリポジトリに保存し、実行時に単一の秘密鍵で復号します。 ```bash # Generate the keypair; commit the public key, keep the private key out of the image php bin/console secrets:generate-keys # Encrypt a value into config/secrets/prod/ php bin/console secrets:set DATABASE_URL # In production, expose only the decryption key to the container export SYMFONY_DECRYPTION_SECRET="$(cat config/secrets/prod/prod.decrypt.private.php)" ``` Dockerfile 内の `composer dump-env prod` のステップは `.env` ファイル群を最適化された単一の `.env.local.php` にコンパイルし、起動時に dotenv ファイルを解析する実行時コストを取り除きます。実際のコンテナ環境で設定された変数は、コンパイル済みの既定値を依然として上書きするため、オーケストレーターが提供するシークレットが常に優先されます。[Symfony デプロイガイド](https://symfony.com/doc/current/deployment.html) が優先順位の全体像を説明していますが、実務上のルールはシンプルです。`APP_SECRET` やデータベース認証情報を決してイメージに焼き込まず、コンテナ起動時に注入することです。 ## Symfony 本番イメージの最適化 本番のパフォーマンス差の大半は 2 つの OPcache 設定で説明できます。タイムスタンプ検証の無効化と、プリロードの有効化です。コンテナイメージは不変であるため、ソースが実行時に変わることはなく、OPcache が編集を確認するためにファイルを stat する必要は一切ありません。プリロードはさらに踏み込んで、Symfony とアプリケーションのクラスをサーバー起動時に一度だけ共有メモリへ読み込みます。 ```ini ; docker/php/prod.ini opcache.enable=1 opcache.preload=/app/var/cache/prod/App_KernelProdContainer.preload.php opcache.preload_user=root opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 ; immutable image: never re-check the filesystem realpath_cache_size=4096K realpath_cache_ttl=600 ``` Symfony は上に挙げた `preload.php` ファイルを `cache:warmup` の際に生成するため、ビルド後にはこのファイルがすでにイメージ内に存在します。[OPcache プリロード](https://www.php.net/manual/en/opcache.preloading.php) はこれらのクラスを起動時にリンクし、それらを必要とする最初のリクエストでのコンパイル手順を省きます。`validate_timestamps=0` の設定が安全なのは、新しいリリースが新しいイメージを意味する不変デプロイに限られます。可変ホストでは古いコードを配信してしまいます。`max_accelerated_files` を実際のファイル数より大きく設定しておけば、フレームワーク全体がエビクションされることなくキャッシュされ続けます。 ## .dockerignore による Symfony イメージサイズの削減 `.dockerignore` ファイルが欠けていると、ビルドは静かに肥大化します。それがなければ、`COPY . .` のステップはローカルの `vendor` ディレクトリ、開発時の `var/cache`、ノードのビルド出力、そして `.git` の履歴をそのままイメージとビルドコンテキストへ送り込みます。これらを除外すればイメージが小さくなり、ビルドコンテキストのデーモンへのアップロードが速くなり、開発時の成果物が新たにインストールされた本番依存を上書きするのを防げます。 ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` もっとも重要なのは `/vendor/` を無視することです。本番ステージは `composer install --no-dev` を実行するため、ホストの開発仕様の vendor ディレクトリを送り込むと、そのステップが完全に台無しになってしまいます。`/var/` を除外すれば開発時のキャッシュとログがイメージから排除され、ビルド時の `cache:warmup` がクリーンな本番キャッシュを生成できます。マルチステージビルドと Alpine ベースの拡張と組み合わせれば、無駄のない Symfony イメージは通常 150 MB を十分に下回ります。 ## Symfony コンテナのデプロイとマイグレーションの実行 本番用の Compose ファイルは、ローカルでビルドするのではなくレジストリのビルド済みイメージを参照し、応答するコンテナにのみオーケストレーターがトラフィックをルーティングするようにヘルスチェックを定義します。シークレットはイメージレイヤーとしてではなく、環境変数として届けられます。 ```yaml # compose.prod.yaml services: php: image: registry.example.com/app:${TAG} environment: APP_ENV: prod APP_SECRET: ${APP_SECRET} DATABASE_URL: ${DATABASE_URL} healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost/health"] interval: 10s timeout: 3s retries: 3 restart: unless-stopped ``` ヘルスチェックは `/health` ルートに curl を送るため、アプリケーションはそのルートを公開する必要があります。データベースに触れずに 200 を返す最小限のコントローラーであれば、チェックは高速に保たれ、データベースの一時的な不調のあいだにコンテナが異常と判定されるのを避けられます。データベースの準備状態が重要な場合は、2 つ目のより深いプローブが軽量なクエリを実行できますが、生存確認のエンドポイントは単純なままにしておきます。 ```php // src/Controller/HealthController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; final class HealthController extends AbstractController { #[Route('/health', name: 'health', methods: ['GET'])] public function __invoke(): JsonResponse { return new JsonResponse(['status' => 'ok']); } } ``` データベースのマイグレーションは、アプリケーションの起動中ではなく、新しいコンテナがトラフィックを受け取る前の独立したステップとして実行するべきです。使い捨てのコンテナで実行すれば、複数のアプリケーションレプリカが並行して起動する場合でも、スキーマがリリースごとにちょうど一度だけ最新化されることが保証されます。 ```bash # deploy.sh set -euo pipefail docker compose -f compose.prod.yaml pull # Apply migrations once, in an isolated container docker compose -f compose.prod.yaml run --rm php \ bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration # Start replicas and wait for the health check to pass docker compose -f compose.prod.yaml up -d --wait ``` `--wait` フラグはすべてのサービスが正常を報告するまでブロックし、投げっぱなしの起動ではなく本物の成功シグナルをデプロイスクリプトに与えます。これを Kubernetes や Docker Swarm のようなオーケストレーターでのローリングアップデートと組み合わせれば、ゼロダウンタイムのリリースが実現します。新しいコンテナがヘルスチェックを通過するまで、古いコンテナが配信を続けるからです。このワークフローが対象とするフレームワークバージョンの全体像については、[Symfony プラットフォーム概要](/technologies/symfony) と [Symfony 8 と PHP 8.4 の機能ガイド](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) が、現行リリースラインで何が変わったのかを扱っています。 ## まとめ - マルチステージ Dockerfile を使い、開発イメージと本番イメージが 1 つのベースを共有しつつ、本番ターゲットは Composer の開発依存なしで配布する - ソースより先に `composer.json` とロックファイルをコピーし、コード変更をまたいで依存インストールのレイヤーをキャッシュに保つ - FrankenPHP をワーカーモードで実行して起動済みカーネルをリクエスト間で再利用し、`ResetInterface` で状態を持つサービスをリセットして状態漏れを防ぐ - ビルド時に Symfony キャッシュをウォームアップし、`dump-env prod` で `.env` をコンパイルして、稼働するコンテナが完全に初期化された状態で起動するようにする - 本番では OPcache プリロードを有効化し `validate_timestamps=0` を設定する。これはイメージが不変であるからこそ安全である - `APP_SECRET`、データベース認証情報、ボルトの復号鍵をイメージレイヤーから排除し、コンテナ起動時に環境変数として注入する - Doctrine マイグレーションは新しいレプリカがトラフィックを受け取る前に使い捨てのコンテナで適用し、`up -d --wait` でヘルスチェックを条件にロールアウトを制御する --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/symfony/symfony-docker-development-production