2026年の Symfony と Docker:開発環境と本番デプロイ
2026年版の完全な Symfony Docker ワークフロー。FrankenPHP 開発環境、マルチステージ本番イメージ、ワーカーモード、暗号化シークレット、イメージ最適化、マイグレーションを伴うゼロダウンタイムデプロイまでを解説します。

この Symfony Docker チュートリアルでは、コンテナワークフロー全体を構築します。再現性の高いローカル開発環境と堅牢な本番イメージの両方を、FrankenPHP と Symfony 7.4 LTS を中心に組み立てます。Symfony をコンテナで動かせば、ノート PC とサーバーのあいだで生じる古典的な環境差異が解消され、各デプロイは壊れやすい手動サーバーコマンドの連続実行ではなく、単一の不変アーティファクトを配布するだけの作業になります。
公式の Symfony Docker テンプレートは、従来の Nginx と PHP-FPM の組み合わせに代わり、ランタイムとして FrankenPHP を採用するようになりました。単一のバイナリが HTTP/2 と HTTP/3 を配信し、リクエストごとにカーネルを再構築するのではなく起動状態のまま保持することで、Symfony を永続的なワーカーモードで実行します。
Symfony 向け Docker Compose 開発環境
良い開発環境は、ホストマシンに手を加えることなく、すべての貢献者に同じ PHP バージョン、同じ拡張、同じデータベースを提供します。Docker Compose はそのスタックを宣言的に記述します。以下の例では、ローカルの Dockerfile からビルドした FrankenPHP コンテナと PostgreSQL 17 サービスを組み合わせ、既定の Compose ネットワーク上で接続しています。
# 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 は共通のベースを共有したうえで、開発ターゲットと本番ターゲットに分岐します。本番ステージはランタイム依存のみをインストールし、Composer の開発用パッケージを除外し、ビルド時にキャッシュをウォームアップするため、稼働するコンテナは即座に起動します。
# 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 /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 ブートストラップを通じてこれを実現します。
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 ワーカー に求められる規律と同じで、その見返りとして同一ハードウェアで 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の面接対策はできていますか?
インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。
本番環境での環境変数とシークレットの管理
Symfony は環境変数から設定を読み込みます。本番でそれらを供給する健全な方法は 2 つあります。1 つ目はオーケストレーターや Compose ファイルを通じてプレーンな変数を注入する方法です。2 つ目は Symfony の暗号化シークレットボルトで、機微な値を暗号文としてリポジトリに保存し、実行時に単一の秘密鍵で復号します。
# 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 デプロイガイド が優先順位の全体像を説明していますが、実務上のルールはシンプルです。APP_SECRET やデータベース認証情報を決してイメージに焼き込まず、コンテナ起動時に注入することです。
Symfony 本番イメージの最適化
本番のパフォーマンス差の大半は 2 つの OPcache 設定で説明できます。タイムスタンプ検証の無効化と、プリロードの有効化です。コンテナイメージは不変であるため、ソースが実行時に変わることはなく、OPcache が編集を確認するためにファイルを stat する必要は一切ありません。プリロードはさらに踏み込んで、Symfony とアプリケーションのクラスをサーバー起動時に一度だけ共有メモリへ読み込みます。
; 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=600Symfony は上に挙げた preload.php ファイルを cache:warmup の際に生成するため、ビルド後にはこのファイルがすでにイメージ内に存在します。OPcache プリロード はこれらのクラスを起動時にリンクし、それらを必要とする最初のリクエストでのコンパイル手順を省きます。validate_timestamps=0 の設定が安全なのは、新しいリリースが新しいイメージを意味する不変デプロイに限られます。可変ホストでは古いコードを配信してしまいます。max_accelerated_files を実際のファイル数より大きく設定しておけば、フレームワーク全体がエビクションされることなくキャッシュされ続けます。
.dockerignore による Symfony イメージサイズの削減
.dockerignore ファイルが欠けていると、ビルドは静かに肥大化します。それがなければ、COPY . . のステップはローカルの vendor ディレクトリ、開発時の var/cache、ノードのビルド出力、そして .git の履歴をそのままイメージとビルドコンテキストへ送り込みます。これらを除外すればイメージが小さくなり、ビルドコンテキストのデーモンへのアップロードが速くなり、開発時の成果物が新たにインストールされた本番依存を上書きするのを防げます。
# .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 ファイルは、ローカルでビルドするのではなくレジストリのビルド済みイメージを参照し、応答するコンテナにのみオーケストレーターがトラフィックをルーティングするようにヘルスチェックを定義します。シークレットはイメージレイヤーとしてではなく、環境変数として届けられます。
# 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 つ目のより深いプローブが軽量なクエリを実行できますが、生存確認のエンドポイントは単純なままにしておきます。
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']);
}
}データベースのマイグレーションは、アプリケーションの起動中ではなく、新しいコンテナがトラフィックを受け取る前の独立したステップとして実行するべきです。使い捨てのコンテナで実行すれば、複数のアプリケーションレプリカが並行して起動する場合でも、スキーマがリリースごとにちょうど一度だけ最新化されることが保証されます。
# 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 プラットフォーム概要 と Symfony 8 と PHP 8.4 の機能ガイド が、現行リリースラインで何が変わったのかを扱っています。
まとめ
- マルチステージ Dockerfile を使い、開発イメージと本番イメージが 1 つのベースを共有しつつ、本番ターゲットは Composer の開発依存なしで配布する
- ソースより先に
composer.jsonとロックファイルをコピーし、コード変更をまたいで依存インストールのレイヤーをキャッシュに保つ - FrankenPHP をワーカーモードで実行して起動済みカーネルをリクエスト間で再利用し、
ResetInterfaceで状態を持つサービスをリセットして状態漏れを防ぐ - ビルド時に Symfony キャッシュをウォームアップし、
dump-env prodで.envをコンパイルして、稼働するコンテナが完全に初期化された状態で起動するようにする - 本番では OPcache プリロードを有効化し
validate_timestamps=0を設定する。これはイメージが不変であるからこそ安全である APP_SECRET、データベース認証情報、ボルトの復号鍵をイメージレイヤーから排除し、コンテナ起動時に環境変数として注入する- Doctrine マイグレーションは新しいレプリカがトラフィックを受け取る前に使い捨てのコンテナで適用し、
up -d --waitでヘルスチェックを条件にロールアウトを制御する
今すぐ練習を始めましょう!
面接シミュレーターと技術テストで知識をテストしましょう。
Symfony のバグを見つけられますか
実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

執筆
Anthony Fillion-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年7月6日 更新
タグ
共有
関連記事

API Platform Symfony REST:完全チュートリアルと面接対策 2026年版
API Platform 4.3を使用してSymfonyでREST APIを構築する方法を解説します。State Provider/Processorパターン、カスタムフィルター、セキュリティ設定、および技術面接で頻出する質問と回答を網羅した実践ガイドです。

Symfony 8の新機能を徹底解説:PHP 8.4レイジーオブジェクト、マルチステップフォーム、面接対策まで
Symfony 8はPHP 8.4を必須とし、ネイティブレイジーオブジェクト、AbstractFlowType、呼び出し可能コマンドなど多数の新機能を搭載しています。本記事では主要機能をコード例とともに解説し、2026年の面接対策ポイントも紹介します。

Doctrine ORM:Symfonyにおけるリレーションのマスター
SymfonyにおけるDoctrine ORMリレーションの完全ガイド。OneToMany、ManyToMany、ロード戦略、パフォーマンス最適化を実例とともに解説します。