【Docker】Nginx + PHP-FPM完全ガイド|FastCGI・pm.max_children計算・OPcache/JIT・Xdebug 3・フレームワーク別rewrite

【Docker】Nginx + PHP-FPMの環境をComposeで構築する手順 Docker

Nginx + PHP-FPMは、PHPアプリの本番環境で事実上の標準となっている構成です。Nginxが静的配信・リバースプロキシ・TLS終端を担い、PHP-FPMがPHPの実行に集中する責務分離により、Apache+mod_phpよりもメモリ効率・並列性・セキュリティで優位に立ちます。LaravelのCloud環境/Forge/Ploi、WordPress商用ホスティング、Symfony/CodeIgniterなど2026年時点のPHP本番サーバーはほぼ全てこの構成です。

多くの入門記事は「動くcompose.ymlを1つ載せる」までで終わっており、実案件で必要になるFastCGIプロトコル・PHP-FPMプロセス管理(pm.max_children/dynamic/ondemand)・OPcache完全設定(JIT含む)・Composer統合・Xdebug連携・アップロードサイズ4箇所の統一・フレームワーク別rewriteルール・セキュリティヘッダーまでまとまった情報がほとんどありません。

この記事では、Nginx + PHP-FPMアーキテクチャの仕組みの本質から、本番相当の完全版compose.yml・nginx.conf・Dockerfile(Composer統合済み)、PHP-FPMプロセス管理チューニング、OPcache+JIT、Xdebug VS Code連携、Laravel/Symfony/WordPress/CodeIgniterのフレームワーク別rewrite、セキュリティ強化、TCP vs Unix socket選定、トラブル対処まで、2026年の現場で即使える完全ガイドとしてまとめます。一般的なNginx活用は【Docker】Nginxローカル開発環境構築完全ガイド、WordPress特化は【Docker】WordPress環境構築完全ガイド、MySQL操作は【Docker】MySQL CLI完全ガイドで補完できます。

この記事で学べること

  • Apache+mod_php vs Nginx+PHP-FPMの比較と選定理由
  • FastCGIプロトコルの仕組みとNginx/PHP-FPM通信の裏側
  • 本番相当の完全版compose.ymlDockerfile(Composer統合済み)
  • nginx.conf実戦設定:フレームワーク別rewrite(Laravel/Symfony/WordPress/CodeIgniter)
  • PHP-FPMのプロセス管理pm.max_children計算式/static/dynamic/ondemand)
  • OPcache + JITの完全設定(開発/本番別)
  • Xdebug 3の設定とVS Code連携(pathMappings含む)
  • Composerのmulti-stage統合(開発/本番/CI)
  • アップロードサイズ4箇所の統一(nginx/php.ini/fpm.conf/client_max_body_size)
  • セキュリティ(expose_php=Offsession.cookie_secure/CSP)
  • TCP vs Unix socketのfastcgi_pass選定(パフォーマンス比較)
  • 典型トラブル(502/白画面/ダウンロードダイアログ/タイムアウト)の原因と対処
スポンサーリンク

Nginx + PHP-FPMアーキテクチャ:なぜ本番標準なのか

PHPの実行環境はApache+mod_phpで長らく使われてきましたが、2010年代後半からNginx+PHP-FPMが本番標準になりました。アーキテクチャの違いが性能・拡張性・セキュリティに直結します。

比較軸 Apache + mod_php Nginx + PHP-FPM
プロセスモデル ApacheワーカーにPHPが埋め込み(全プロセスがPHP所持) Nginxは軽量イベント駆動、PHPは別プロセスプール
メモリ使用量 ×(静的配信でもPHP分メモリ確保) ◯(静的配信はNginxのみ)
同時接続処理 スレッド/プロセス(C10k問題) ◎ イベント駆動(10万接続も可)
静的配信速度 ◎ 圧倒的に高速
設定の柔軟性 .htaccess可(便利だが遅い) 設定ファイルのみ(高速)
責務分離 密結合 疎結合(Webとアプリの別スケール可)
2026年の採用率 減少傾向 本番標準

FastCGIプロトコルの仕組み

通信の流れ
ブラウザ
   ↓ HTTP
Nginx(静的ファイルならここで配信完了)
   ↓ FastCGIプロトコル(TCPまたはUnix socket)
PHP-FPMプロセスプール
   ↓ PHP実行
アプリケーション(Laravel等)
   ↓ DB/Redis/外部API等
結果をFastCGI経由でNginxに返す
   ↓ HTTPレスポンス
ブラウザ

FastCGIとCGIの違い

CGIは毎リクエストでプロセス起動→終了(超遅い)。FastCGIはプロセスをプールとして常駐させ、リクエストが来るたびプール内の待機プロセスに割り当てます。これによりプロセス起動コストが消え、PHPの実行性能が劇的に向上。PHP-FPMはFastCGIプロトコルの具体的な実装です。

30秒クイックスタート:最小構成

ディレクトリ構成
project/
├─ compose.yml
├─ docker/
│  ├─ nginx/
│  │  └─ default.conf
│  └─ php/
│     ├─ Dockerfile
│     ├─ php.ini
│     └─ www.conf
└─ app/
   └─ public/
      └─ index.php
compose.yml(最小)
services:
  nginx:
    image: nginx:1.27-alpine
    ports: ["8080:80"]
    volumes:
      - ./app:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      php:
        condition: service_healthy

  php:
    build: ./docker/php
    volumes:
      - ./app:/var/www/html
    healthcheck:
      test: ["CMD", "php-fpm", "-t"]
      interval: 10s
      retries: 5
docker/php/Dockerfile(最小)
FROM php:8.3-fpm-alpine

RUN apk add --no-cache \
    icu-dev libpng-dev libjpeg-turbo-dev libzip-dev oniguruma-dev \
 && docker-php-ext-configure intl \
 && docker-php-ext-install -j$(nproc) intl opcache pdo pdo_mysql mbstring zip gd

COPY php.ini /usr/local/etc/php/conf.d/zzz-custom.ini
COPY www.conf /usr/local/etc/php-fpm.d/zzz-www.conf

WORKDIR /var/www/html
docker/nginx/default.conf(最小)
server {
  listen 80;
  server_name localhost;

  root /var/www/html/public;
  index index.php;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location ~ \.php$ {
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_read_timeout 60s;
  }
}
app/public/index.php(動作確認)
<?php
phpinfo();
起動
docker compose up -d --build
# http://localhost:8080 で phpinfo() が表示されれば成功

2026年の推奨構成:nginx:1.27-alpinephp:8.3-fpm-alpine。alpineは軽量(約80MB)でマイナー固定しておけば自動でセキュリティパッチが当たります。PHP 7系はEOL済み、8.0/8.1もサポート終了に近いので8.3以上が2026年標準。

本番相当の完全版:Dockerfile(Composer統合)+ compose.yml

Dockerfile:multi-stageでComposer統合

docker/php/Dockerfile(multi-stage)
# ステージ1: Composer依存解決(CI/本番で変更頻度低い)
FROM composer:2 AS composer

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev --no-interaction --no-progress --no-scripts \
    --prefer-dist --optimize-autoloader

# ステージ2: PHP-FPM本体
FROM php:8.3-fpm-alpine

# 必要拡張を一括インストール
RUN apk add --no-cache \
    icu-dev libpng-dev libjpeg-turbo-dev libzip-dev oniguruma-dev \
    postgresql-dev linux-headers \
 && docker-php-ext-configure intl \
 && docker-php-ext-install -j$(nproc) \
    intl opcache pdo pdo_mysql pdo_pgsql mbstring zip gd bcmath pcntl sockets \
 && pecl install redis \
 && docker-php-ext-enable redis \
 && apk del --no-cache icu-dev libpng-dev libjpeg-turbo-dev libzip-dev postgresql-dev linux-headers

# Composerのバイナリをコピー
COPY --from=composer /usr/bin/composer /usr/bin/composer

# 設定ファイル
COPY php.ini /usr/local/etc/php/conf.d/zzz-custom.ini
COPY www.conf /usr/local/etc/php-fpm.d/zzz-www.conf

WORKDIR /var/www/html

# 非rootユーザーで起動
USER www-data

EXPOSE 9000

compose.yml(本番相当)

compose.yml
services:
  nginx:
    image: nginx:1.27-alpine
    ports: ["8080:80"]
    volumes:
      - ./app:/var/www/html
      - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./docker/nginx/snippets:/etc/nginx/snippets:ro
      - nginx-cache:/var/cache/nginx
    depends_on:
      php:
        condition: service_healthy
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost/healthz || exit 1"]
      interval: 10s
      retries: 5
    networks: [app-net]

  php:
    build:
      context: ./docker/php
      target: dev                    # multi-stageのdev段階
    environment:
      PHP_IDE_CONFIG: "serverName=docker"
      TZ: "Asia/Tokyo"
    volumes:
      - ./app:/var/www/html
    healthcheck:
      test: ["CMD", "php-fpm", "-t"]
      interval: 10s
      retries: 5
    networks: [app-net]

  db:
    image: mariadb:11
    environment:
      MARIADB_DATABASE: app
      MARIADB_USER: app
      MARIADB_PASSWORD: app
      MARIADB_ROOT_PASSWORD: root
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect"]
      interval: 5s
      retries: 10
    networks: [app-net]

  redis:
    image: redis:7-alpine
    networks: [app-net]

volumes:
  db_data:
  nginx-cache:

networks:
  app-net:

nginx.conf完全版+フレームワーク別rewrite

実戦的なdefault.conf

docker/nginx/default.conf
server {
  listen 80;
  server_name localhost;

  root /var/www/html/public;
  index index.php;

  charset utf-8;
  client_max_body_size 20M;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;

  # ヘルスチェック(depends_onのhealthcheck用)
  location = /healthz {
    access_log off;
    return 200 "ok";
    add_header Content-Type text/plain;
  }

  # 静的ファイルは長期キャッシュ
  location ~* \.(?:css|js|woff2?|ttf|otf|ico|png|jpg|jpeg|gif|webp|svg)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
    access_log off;
    try_files $uri =404;
  }

  # フロントコントローラ(Laravel等)
  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  # PHP実行
  location ~ \.php$ {
    try_files $uri =404;                # 存在しないphpは404
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PATH_INFO $fastcgi_path_info;
    fastcgi_read_timeout 60s;
    fastcgi_buffers 16 16k;
    fastcgi_buffer_size 32k;
  }

  # .htaccess等を外部から見せない
  location ~ /\.(?!well-known).* {
    deny all;
  }
}

フレームワーク別rewriteルール

フレームワークによってlocation /内のtry_filesが微妙に違います。以下に主要な4つを一覧で提示します。

Laravel / Symfony / WordPress / CodeIgniter
# Laravel / Symfony(/index.phpへフロントコントローラ)
location / {
  try_files $uri $uri/ /index.php?$query_string;
}

# WordPress(index.phpへ、?にパスを渡さない)
location / {
  try_files $uri $uri/ /index.php?$args;
}

# CodeIgniter 4(publicディレクトリから)
location / {
  try_files $uri $uri/ /index.php?$uri&$args;
}

# Yii2(statselbypublic)
location / {
  try_files $uri $uri/ /index.php$is_args$args;
}

snippets パターンで共通化

docker/nginx/snippets/fastcgi-php.conf
# 複数serverブロックで同じPHP設定を使う
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_read_timeout 60s;

# default.conf内で使う
# location ~ \.php$ {
#   include /etc/nginx/snippets/fastcgi-php.conf;
# }

PHP-FPMプロセス管理:pm.max_children計算式

FPMの設定で最も重要なのがpm(プロセスマネージャ)。多すぎるとOOM、少なすぎると503エラー。用途に応じた適切な値を理解しましょう。

docker/php/www.conf(php-fpm設定)
[www]
user = www-data
group = www-data
listen = 9000
listen.allowed_clients = 127.0.0.1

; プロセスマネージャ
pm = dynamic
pm.max_children = 10            ; 最大プロセス数
pm.start_servers = 3            ; 起動時のプロセス数
pm.min_spare_servers = 2        ; アイドル最小
pm.max_spare_servers = 5        ; アイドル最大
pm.max_requests = 500           ; メモリリーク予防でN回で再起動

; ステータスページ(監視用)
pm.status_path = /fpm-status

; ping(healthcheck用)
ping.path = /fpm-ping

; ログ
access.log = /proc/self/fd/2
catch_workers_output = yes
decorate_workers_output = no

; タイムアウト
request_terminate_timeout = 60s
request_slowlog_timeout = 10s
slowlog = /proc/self/fd/2

pm.max_childrenの計算式

計算例
pm.max_children = (利用可能RAM - 予約分) / 1プロセスの平均メモリ

例: コンテナメモリ1GB、1プロセス50MB使用、OSとopcache等で200MB予約
    (1024 - 200) / 50 = 16プロセス → 切り下げて pm.max_children = 14

# プロセスの実メモリを確認
docker compose exec php ps -o pid,rss,cmd ax

pm モード3種の使い分け

モード 挙動 向いているケース
static pm.max_children固定で起動 本番高トラフィック/予測可能な負荷
dynamic(既定) 負荷に応じてmin/max間で可変 一般的な本番サーバー
ondemand リクエスト時にだけ起動 低負荷/開発環境/メモリ節約

開発中はondemandでメモリ消費最小化、本番はdynamicで予熱+自動調整、大量同時接続の本番はstaticで安定化、が実戦的な使い分け。

OPcache + JITの完全設定

OPcacheはPHPコンパイル結果をメモリキャッシュする機能で、有効化するだけで30〜50%高速化。PHP 8のJITと組み合わせれば数値計算系はさらに2〜3倍。

開発用設定

php.ini(開発)
; === OPcache 開発用 ===
opcache.enable = 1
opcache.enable_cli = 0
opcache.validate_timestamps = 1      ; ファイル更新チェック
opcache.revalidate_freq = 0          ; 即時反映(開発向き)
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 10000
opcache.fast_shutdown = 1

; === 開発で役立つ設定 ===
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
log_errors = On
error_log = /proc/self/fd/2          ; Dockerログへ

本番用設定(JIT含む)

php.ini(本番)
; === OPcache 本番用 ===
opcache.enable = 1
opcache.enable_cli = 1
opcache.validate_timestamps = 0      ; ファイルチェック無効(超速)
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 20000
opcache.fast_shutdown = 1
opcache.save_comments = 1            ; attribute等で必要
opcache.preload = /var/www/html/preload.php
opcache.preload_user = www-data

; === JIT(PHP 8+) ===
opcache.jit_buffer_size = 128M
opcache.jit = tracing                ; tracing が一般的

; === セキュリティ ===
expose_php = Off
display_errors = Off
log_errors = On
error_log = /proc/self/fd/2

opcache.validate_timestamps = 0は本番専用。開発でこれを設定するとファイルを変更しても反映されず困惑します。本番でデプロイ後はdocker compose exec php php -r "opcache_reset();"で明示リセットするか、FPMプロセスリロード(docker compose exec php kill -USR2 1)でOPcacheを更新します。

JITのトレードオフ

JITは数値計算・画像処理・機械学習等CPUバウンドな処理で劇的な効果を発揮しますが、一般的なWebアプリ(DB I/Oが律速)では数%程度の改善。それでもJITによる副作用(バグ)は過去に何度か報告されているため、ミッションクリティカルなアプリはjit = disableで安全側に寄せる選択もあります。

Xdebug 3 + VS Code ステップ実行デバッグ

Xdebug対応Dockerfile

docker/php/Dockerfile に追記
# 既存の拡張インストールに続けて追加
RUN pecl install xdebug \
 && docker-php-ext-enable xdebug

COPY xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
docker/php/xdebug.ini
zend_extension=xdebug

; Xdebug 3 の新形式
xdebug.mode = debug,develop
xdebug.start_with_request = yes
xdebug.client_host = host.docker.internal
xdebug.client_port = 9003
xdebug.idekey = VSCODE
xdebug.log = /tmp/xdebug.log

; 開発では discover_client_host をOFFで固定
xdebug.discover_client_host = 0
compose.yml(Xdebug対応)
services:
  php:
    build: ./docker/php
    extra_hosts:
      - "host.docker.internal:host-gateway"   # Linux対応
    # Xdebug用ポートは不要(phpからホストへ接続する方向)
.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Listen for Xdebug (Docker)",
      "type": "php",
      "request": "launch",
      "port": 9003,
      "pathMappings": {
        "/var/www/html": "${workspaceFolder}/app"
      },
      "xdebugSettings": {
        "max_children": 128,
        "max_data": 1024,
        "max_depth": 5
      }
    }
  ]
}

Xdebug 3 のポート変更点

Xdebug 2 の既定ポートは9000でしたが、Xdebug 3から9003に変更されました。PHP-FPMの9000番と競合回避が目的。Xdebug 2の古い記事を参考にすると設定が動かない原因になりやすい。

Xdebugを常時ONにすると動作が重い(2〜3倍遅くなる)ため、本番や通常テストではxdebug.mode = offに切り替え。compose.override.ymlで開発時だけ有効化するのが理想です。

Composer統合パターン:multi-stage/同居/専用コンテナ

パターン①:multi-stage(本番推奨)

Dockerfileで本番用にdeps焼き込み
# 前述のmulti-stage Dockerfile参照
# composerステージでインストール → php-fpmステージにvendor/をCOPY

COPY --from=composer --chown=www-data:www-data /app/vendor /var/www/html/vendor

パターン②:同居(開発で使いやすい)

開発用Dockerfileに組み込む
# phpコンテナ内でcomposerも使えるように
RUN curl -sS https://getcomposer.org/installer | php -- \
    --install-dir=/usr/local/bin --filename=composer

# 実行例
docker compose exec php composer install
docker compose exec php composer require monolog/monolog
docker compose exec php composer dump-autoload

パターン③:専用コンテナ(profiles)

compose.yml(composer専用サービス)
services:
  php:
    # ... 本体

  composer:
    image: composer:2
    volumes:
      - ./app:/app
    working_dir: /app
    profiles: ["tools"]          # 通常起動しない
    command: install
composer専用コンテナで実行
# 依存インストール
docker compose run --rm composer install

# パッケージ追加
docker compose run --rm composer require laravel/sanctum

2026年の推奨:開発はパターン②(phpコンテナに同居)、本番はパターン①(multi-stage)。パターン③は「ホストにPHP入れたくない/専用CIジョブ」で有用。Laravel Sail等の標準もphpコンテナに同居させる方式です。

アップロードサイズ4箇所の統一:最大の罠

「アップロードできない」原因は4箇所のどれかだけ小さいことがほとんど。全てを同じ値に揃えないと最小値でブロックされます。

4箇所統一パターン(例:128M)
# ① nginx.conf(最優先で弾かれる)
http {
  client_max_body_size 128M;
}
# または server{} / location{} 個別指定も可

# ② php.ini
upload_max_filesize = 128M
post_max_size = 130M                 ; upload_max_filesize より大きく
memory_limit = 256M                  ; post_max_size以上が望ましい

# ③ php-fpm www.conf
; request_terminate_timeout が短いと大ファイルで中断
request_terminate_timeout = 300

# ④ フレームワーク側(Laravel例)
# Laravelの form validation でも max:131072 (KB) 設定
# WordPress wp-admin → 設定 → メディア でサーバー上限表示

post_max_sizeはupload_max_filesizeより大きく設定すること。両方同じだとフォームデータ全体でギリギリオーバーすることが。さらにmemory_limitはpost_max_sizeより大きくするのが基本。

セキュリティ強化:最低限入れるべき設定

php.ini のセキュリティ設定

php.ini(セキュリティ)
; PHPバージョンを隠す(Server: PHP/8.3.xを消す)
expose_php = Off

; ディレクトリ制限(アップロード先/キャッシュだけ許可)
open_basedir = "/var/www/html:/tmp:/usr/share/php"

; 危険な関数を無効化
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_multi_exec,parse_ini_file,show_source

; セッションのセキュア設定
session.cookie_httponly = 1
session.cookie_secure = 1            ; HTTPS必須
session.cookie_samesite = Lax
session.use_strict_mode = 1
session.sid_length = 48
session.sid_bits_per_character = 6

; ファイルアップロードの制限
file_uploads = On
upload_tmp_dir = /var/www/html/tmp

; エラー情報を外部に漏らさない
display_errors = Off
display_startup_errors = Off
log_errors = On

nginx側のセキュリティヘッダー

docker/nginx/snippets/security-headers.conf
# 共通セキュリティヘッダー
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;

# PHPのServer: PHP/... を消す(expose_php=Offとの二重対策)
fastcgi_hide_header X-Powered-By;
more_clear_headers Server;            # headers-more モジュール必要

# HTTPS時
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

サーバー署名(Server: nginx, X-Powered-By: PHP/…)は本番では必ず隠す。バージョン情報が漏れると既知の脆弱性を狙った攻撃の的になります。expose_php=Offfastcgi_hide_header X-Powered-Byserver_tokens off;(nginx)の3点セットで対応。

fastcgi_pass:TCP vs Unix socket

Nginx→PHP-FPMの通信には2方式あり、性能と運用性にトレードオフがあります。

方式 特徴 向いているケース
TCP
(fastcgi_pass php:9000;)
別コンテナ/別サーバー間もOK、汎用的 Docker Compose(ほぼこれ一択)
Unix socket
(fastcgi_pass unix:/var/run/php-fpm.sock;)
若干高速(TCPオーバーヘッドなし)、同ホスト必須 同一サーバー/ベアメタル/最大性能追求
Unix socket構成(同一コンテナで共有)
# php-fpm.conf
listen = /var/run/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

# nginx.conf
fastcgi_pass unix:/var/run/php-fpm.sock;

# compose.ymlでsocketディレクトリを両コンテナで共有
volumes:
  - php-socket:/var/run

Dockerでの現実的な選択

Dockerではnginx・php-fpmを別コンテナにするのが定石で、異なるコンテナ間でUnix socketを共有するのは手間の割に効果が小さい。TCP(fastcgi_pass php:9000;)が圧倒的に楽でシンプル。Unix socketが本当に必要なのはベアメタル本番の極限チューニングくらいです。

典型トラブルと対処

① 502 Bad Gateway

原因:①PHPコンテナが未起動/クラッシュ、②fastcgi_passのホスト名・ポートミスマッチ、③pm.max_childrenを超える同時接続、④PHP-FPMがsocketモードでnginxがTCPを指している(逆も)。対処:docker compose logs phpdocker compose psでphpの状態確認。depends_on + healthcheckで起動順序を保証。

② 白画面(White Screen of Death)

エラー表示を有効化して診断
# 一時的に display_errors を ON
docker compose exec php sh -c "echo 'display_errors=On' > /usr/local/etc/php/conf.d/temp-errors.ini"
docker compose restart php

# PHP-FPMのエラーログを追跡
docker compose logs -f php

# プロセス中で直接エラーを吐き出す
docker compose exec php tail -f /proc/self/fd/2

③ PHPファイルがダウンロードされる

location ~ \.php$ブロックが一致していないため、Nginxが静的ファイルとして返す→ブラウザがダウンロード扱い。nginx.confの正規表現ミス(例:行末の$忘れ)をチェック。docker compose exec nginx nginx -tで構文チェック。

④ 504 Gateway Timeout

タイムアウトを4箇所で揃える
# ① nginx側
fastcgi_read_timeout 300s;

# ② php.ini
max_execution_time = 300
max_input_time = 300

# ③ php-fpm www.conf
request_terminate_timeout = 300

# ④ フレームワーク側のタイムアウト設定

⑤ アップロードできない

本記事「アップロードサイズ4箇所の統一」を参照。もう1つの原因はopen_basedirの制限——アップロード先ディレクトリがopen_basedirの範囲外だと失敗します。

⑥ OPcacheがリロードされない

OPcacheリセット
# opcache.validate_timestamps=0 の本番で
docker compose exec php php -r "opcache_reset();"

# または FPMプロセスをreload(graceful restart)
docker compose exec php kill -USR2 1

# コンテナを再起動してもOK
docker compose restart php

よくある質問

Qなぜmod_phpよりPHP-FPMなのか?
Amod_phpはApacheの全プロセスにPHP機能が埋め込まれるため、静的ファイル配信のワーカーまでPHPのメモリを抱え込みます。PHP-FPMはPHP実行を別プロセスに分離するので、静的配信ではメモリゼロでNginxだけが動作。同じリソースで3〜5倍の同時接続を捌けるため、本番標準になっています。
Qpm.max_childrenの適切な値は?
A(利用可能RAM - OSやOPcacheの予約200MB程度) / 1プロセスの平均メモリ使用量で算出。1プロセスの実メモリはdocker compose exec php ps -o pid,rss axで確認。例:1GB RAMでプロセス50MB使用なら(1024-200)/50 ≈ 16→切り下げて14。詳細は本記事「PHP-FPMプロセス管理」セクション参照。
QOPcacheのvalidate_timestampsは開発/本番で何を変える?
A開発:validate_timestamps = 1revalidate_freq = 0(ファイル更新を即反映)。本番:validate_timestamps = 0(チェック無しで最速)。ただし本番デプロイ時はopcache_reset()kill -USR2 1でFPMをgracefulリロードしてコードを反映させます。
QJITは有効にすべき?
A数値計算・画像処理・機械学習などCPUバウンドな処理では数倍速くなります。一般的なWebアプリ(DB I/Oが律速)は数%程度の改善。JIT由来のバグ報告も過去にあるため、ミッションクリティカルなアプリはjit = disableで安全側に。新規開発から順次有効化するのが現実的。
QXdebug 2から3へ移行したら動かなくなった
Aポートが90009003に変わり、remote_*設定はclient_*に改名されました。Xdebug 3ではxdebug.mode = debug,developのようにモード指定が必須です。本記事「Xdebug 3」セクションのxdebug.iniをそのまま使えば動きます。
QComposerはどこに入れるべき?
A開発中はphpコンテナに同居docker compose exec php composer installで楽)、本番はmulti-stage Dockerfileでビルド時にインストール(vendor/を焼き込み)。CIならcomposer:2イメージを専用ジョブで走らせる手もあります。目的別に使い分けましょう。
Qアップロードできない(413や圧縮失敗)
A4箇所(nginx client_max_body_size/php.ini upload_max_filesizepost_max_sizememory_limit/php-fpm request_terminate_timeout)を全て同じ値に揃えてください。post_max_size > upload_max_filesizememory_limit >= post_max_sizeの関係も重要。本記事「アップロードサイズ4箇所の統一」参照。
QUnix socketに切り替えるとどれくらい速くなる?
A実測ベースで数%程度(1リクエストあたりμ秒オーダー)。PHP処理自体やDB I/Oのコストが支配的なので、socketにしても体感差はほぼゼロ。Dockerで異なるコンテナ間のsocket共有は手間が多く、TCP(fastcgi_pass php:9000;)推奨です。ベアメタル本番で最後の数%を絞り出す時以外は不要。
Qセキュリティでまず何をすべき?
Aexpose_php = Offfastcgi_hide_header X-Powered-Byでバージョン隠蔽、②server_tokens off;でNginxバージョンも隠す、③disable_functionsで危険関数無効化、④セッションcookieをhttponly=1; secure=1; samesite=Lax、⑤セキュリティヘッダー(CSP・X-Frame-Options・HSTS)。本記事「セキュリティ強化」セクション参照。

関連記事

まとめ

  • Nginx + PHP-FPMは静的配信+PHP実行を責務分離した本番標準構成
  • FastCGIプロトコルでNginxとPHP-FPM間をTCP/Unix socketで通信
  • 推奨イメージ:nginx:1.27-alpine + php:8.3-fpm-alpine
  • Dockerfileはmulti-stagecomposer:2を統合(本番向け)
  • nginx.confはフレームワーク別rewrite(Laravel/Symfony/WordPress/CodeIgniter)を使い分け
  • PHP-FPMはpm = dynamicが既定、pm.max_childrenをRAMから算出
  • OPcache設定:開発はvalidate_timestamps=1、本番は=0opcache_resetデプロイフック
  • JITはCPUバウンドで効果絶大、Web I/O主体なら微増(jit = tracing推奨)
  • Xdebug 3はポート9003client_host=host.docker.internalでVS Code連携
  • アップロードサイズは4箇所(nginx/upload_max/post_max/memory_limit)を揃える
  • セキュリティ:expose_php=Off/セッションsecure/disable_functions/セキュリティヘッダー
  • DockerではTCP fastcgi_passがシンプルで推奨(Unix socketの恩恵は微小)
  • 502/白画面/ダウンロード扱い/504/OPcacheリロードは典型トラブルで対処法が定石化

Nginx + PHP-FPMは本番PHPサーバーの事実上の標準です。本記事のcompose.yml/Dockerfile/nginx.conf/php.ini/www.confを土台にすれば、Laravel・Symfony・WordPress・CodeIgniteいずれのフレームワークでも短時間で本番相当のローカル環境が構築できます。さらにMySQL/Redis/キューを追加し、GitHub Actionsでイメージ自動ビルドに乗せれば、個人開発からチーム開発・本番運用まで一気通貫で設計できます。