2026年の Symfony と Docker:開発環境と本番デプロイ

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

2026年の Symfony Docker チュートリアル:開発環境と本番デプロイ

この Symfony Docker チュートリアルでは、コンテナワークフロー全体を構築します。再現性の高いローカル開発環境と堅牢な本番イメージの両方を、FrankenPHP と Symfony 7.4 LTS を中心に組み立てます。Symfony をコンテナで動かせば、ノート PC とサーバーのあいだで生じる古典的な環境差異が解消され、各デプロイは壊れやすい手動サーバーコマンドの連続実行ではなく、単一の不変アーティファクトを配布するだけの作業になります。

2026年の Symfony Docker スタック

公式の Symfony Docker テンプレートは、従来の Nginx と PHP-FPM の組み合わせに代わり、ランタイムとして FrankenPHP を採用するようになりました。単一のバイナリが 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:entitydocker compose exec php composer require symfony/uid のようになります。ホスト側に PHP をインストールする必要は一切ありません。

Symfony 本番向けマルチステージ Dockerfile

マルチステージ Dockerfile は共通のベースを共有したうえで、開発ターゲットと本番ターゲットに分岐します。本番ステージはランタイム依存のみをインストールし、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 ブートストラップを通じてこれを実現します。

public/index.phpphp
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 の暗号化シークレットボルトで、機微な値を暗号文としてリポジトリに保存し、実行時に単一の秘密鍵で復号します。

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 デプロイガイド が優先順位の全体像を説明していますが、実務上のルールはシンプルです。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 プリロード はこれらのクラスを起動時にリンクし、それらを必要とする最初のリクエストでのコンパイル手順を省きます。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 つ目のより深いプローブが軽量なクエリを実行できますが、生存確認のエンドポイントは単純なままにしておきます。

src/Controller/HealthController.phpphp
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 プラットフォーム概要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-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年7月6日 更新

タグ

#symfony
#docker
#frankenphp
#php
#deployment
#devops

共有

関連記事