2026年6月26日
2026年6月26日
WordPressをゼロダウンタイムでデプロイする方法(リリースディレクトリとシンボリックリンク切り替え)
WordPress のテーマやプラグインの更新でサイトを止めないためには、稼働中のファイルを上書きせず、別のディレクトリに新しいリリースを組み立ててから、公開ディレクトリのシンボリックリンクを一瞬で切り替える方式が基本になります。この記事では、ディレクトリ構成、共有ファイルの扱い、OPcache のリセット、データベース変更の進め方、切り替え後の確認とロールバックまでを、そのまま運用に組み込める形で説明します。
上書きデプロイでサイトが止まる理由
FTP や rsync で本番のファイルを直接上書きすると、転送の途中で「新しいファイルと古いファイルが混ざった状態」が数秒から数分続きます。この間に来たリクエストは、存在しない関数を呼んだり、古いクラス定義を読んだりして、致命的なエラーや真っ白な画面になることがあります。
管理画面からのプラグイン更新も同じ問題を抱えています。WordPress は更新中にサイト直下へ .maintenance ファイルを置き、その間は「メンテナンスのため一時的に利用できません」という趣旨のメッセージを返します。更新が途中で失敗すると .maintenance が残り、手で消すまでサイトが表示されないこともあります。
ゼロダウンタイムを目指すなら、本番で「ファイルを書き換える」作業そのものをなくし、「完成したディレクトリへ参照先を切り替える」だけにするのが近道です。
ディレクトリ構成
よく使われるのは次の形です。Deployer や Capistrano といったデプロイツールも、基本的には同じ構成を作ります。
/var/www/example/
├── current -> releases/20261017093000 # Web サーバーが見る場所(シンボリックリンク)
├── releases/
│ ├── 20261010120000/ # ひとつ前のリリース(ロールバック用)
│ └── 20261017093000/ # 最新のリリース
└── shared/
├── uploads/ # メディアファイル(全リリースで共有)
└── wp-config-secrets.php # DB 接続情報や認証用キー
releases/ には WordPress 本体・テーマ・プラグインなど、Git やビルドで再現できるものだけを置きます。一方、ユーザーがアップロードした画像や、環境ごとに異なる設定値は shared/ に置き、各リリースから参照します。こうしておけば、リリースを丸ごと入れ替えても画像や接続情報は失われません。
wp-config.php の置き方
wp-config.php を shared/ へのシンボリックリンクにする方法もありますが、PHP の __DIR__ や __FILE__ はシンボリックリンクを解決した実体のパスを返すため、設定ファイル内でパスを組み立てていると shared/ 基準になってしまいます。扱いやすいのは、リリース側の wp-config.php はリポジトリで管理し、秘密情報だけを共有ファイルから読み込む形です。
<?php
// releases/xxxx/wp-config.php(リポジトリで管理する)
require '/var/www/example/shared/wp-config-secrets.php'; // DB_NAME, DB_PASSWORD, 認証用キーなどを定義
$table_prefix = 'wp_';
define( 'WP_ENVIRONMENT_TYPE', 'production' );
define( 'DISALLOW_FILE_MODS', true ); // 本番の管理画面からの更新・インストールを止める
if ( ! defined( 'ABSPATH' ) ) {
define( 'ABSPATH', __DIR__ . '/' );
}
require_once ABSPATH . 'wp-settings.php';
DISALLOW_FILE_MODS を有効にすると、管理画面からのプラグイン・テーマの追加と更新、ファイル編集ができなくなります。本番のファイルはデプロイでしか変えない、という運用を守るための設定です。WordPress 本体やプラグインの更新は、ステージングで試したうえでリポジトリ側に取り込み、次のリリースとして出します。
Web サーバーの設定
公開ディレクトリは current を指します。Nginx と PHP-FPM の組み合わせでは、PHP に渡すパスを $document_root ではなく $realpath_root にしておくのがポイントです。
server {
root /var/www/example/current;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}
$document_root のままだと PHP には current/index.php というパスが渡り、PHP 側の realpath キャッシュや OPcache が古いリリースの実体を指し続けることがあります。$realpath_root ならリンクを解決した releases/20261017093000/index.php が渡るため、切り替え直後から新しいリリースのファイルとして扱われます。ソケットのパスは PHP のバージョンやディストリビューションで異なるので、環境に合わせて読み替えてください。Ubuntu 系のパッケージでは php8.x-fpm.sock の形が一般的です(2026年10月現在)。
デプロイスクリプトの例
Git のタグからリリースを作り、確認してから切り替える最小限のスクリプトです。初めて導入するときは、本番と同じ構成のステージングで動作を確かめてください。
#!/usr/bin/env bash
set -euo pipefail
APP=/var/www/example
TAG="${1:?使い方: deploy.sh <タグ名>}"
RELEASE=$(date +%Y%m%d%H%M%S)
NEW="$APP/releases/$RELEASE"
# 1. 新しいリリースを書き出す(uploads はリポジトリに含めない)
mkdir -p "$NEW"
git -C "$APP/repo" fetch --tags
git -C "$APP/repo" archive "$TAG" | tar -x -C "$NEW"
# 2. 共有ディレクトリをつなぐ
ln -s "$APP/shared/uploads" "$NEW/wp-content/uploads"
# 3. 切り替え前の確認(DB に接続できるか、インストール済みか)
wp --path="$NEW" core is-installed
# 4. シンボリックリンクを原子的に切り替える
ln -s "$NEW" "$APP/current.tmp"
mv -T "$APP/current.tmp" "$APP/current"
# 5. PHP-FPM の OPcache をリセットする
cachetool opcache:reset --fcgi=/run/php/php8.3-fpm.sock
echo "deployed: $RELEASE"
切り替えを原子的にする理由
ln -sfn で既存のリンクを上書きする方法もよく見かけますが、内部では「削除してから作成」に近い動きになり、ごく短い時間リンクが存在しない瞬間が生まれます。一時的な名前でリンクを作ってから mv -T で置き換えると、同じファイルシステム内では rename として一度に入れ替わるため、どのリクエストも古いか新しいかのどちらかのリリースだけを見ることになります。mv -T は GNU coreutils のオプションなので、macOS など別の環境で使う場合は挙動を確認してください。
OPcache はコマンドラインからリセットできない
本番では opcache.validate_timestamps=0 にしてファイルの更新確認を止めていることが多く、その場合はリセットしないと古いコードが実行され続けます。注意したいのは、php -r 'opcache_reset();' のように CLI で実行しても、リセットされるのは CLI プロセス自身のキャッシュだけで、PHP-FPM のキャッシュには届かない点です。
PHP-FPM のキャッシュを消す方法は主に2つあります。
- cachetool のように FastCGI 経由で PHP-FPM に
opcache_reset()を実行させるツールを使う systemctl reload php8.3-fpmで PHP-FPM を再読み込みする
再読み込みはワーカーの入れ替えを伴うため、設定によっては処理中のリクエストが中断されることがあります。process_control_timeout で終了を待つ時間を決められるので、再読み込み方式を選ぶ場合は値を確認しておきます。
データベースの変更は「拡張してから縮小」
ファイルは一瞬で切り替えられても、データベースは新旧のリリースで共有しています。切り替えの前後どちらのコードでも動くように、変更を段階に分けるのが expand/contract(拡張と縮小)と呼ばれる進め方です。
- 拡張: 新しいカラムやテーブル、オプションを追加する。古いコードは新しい項目を無視するので壊れない
- 移行: 新旧の両方に書き込むコードをリリースし、既存データを新しい形へ移す
- 縮小: 新しい項目だけを使うコードが安定したら、次以降のリリースで古い項目を削除する
カラムの名前変更や削除を一度のリリースで行うと、切り替え直前の古いコードや、ロールバックした先のコードが動かなくなります。WordPress 本体やプラグインの大きな更新でデータベースの更新処理(wp core update-db など)が必要な場合は、ステージングで所要時間と互換性を確かめ、更新前に wp db export でバックアップを取ってから実行してください。
切り替え後のヘルスチェック
切り替えたら、すぐに外から応答を確かめます。トップページだけでなく、ログイン画面や主要な固定ページ、REST API など、壊れると影響が大きい URL を並べておくと安心です。
for url in https://example.com/ https://example.com/wp-login.php https://example.com/wp-json/; do
code=$(curl -s -o /dev/null -w '%{http_code}' "$url")
echo "$code $url"
[ "$code" = "200" ] || exit 1
done
ステータスコードが 200 でも、PHP の警告が増えていたり、特定のプラグインの画面だけ壊れていたりすることがあります。切り替え直後の数分は PHP と Web サーバーのエラーログも合わせて確認します。
ロールバックと古いリリースの整理
問題が見つかったら、current をひとつ前のリリースに向け直し、OPcache をリセットするだけで戻せます。ファイルの転送が要らないので、数秒で元に戻ります。
PREV=$(ls -1d /var/www/example/releases/* | sort | tail -n 2 | head -n 1)
ln -s "$PREV" /var/www/example/current.tmp
mv -T /var/www/example/current.tmp /var/www/example/current
cachetool opcache:reset --fcgi=/run/php/php8.3-fpm.sock
データベースを変更していた場合は、ファイルだけ戻しても整合しないことがあります。拡張と縮小を分けておけば、多くの場合はファイルを戻すだけで済みます。
リリースは増え続けるので、直近の数世代だけ残して削除します。削除は取り消せないため、現在の current が指すリリースを消さないよう、スクリプト化する場合は対象を表示してから実行する手順にしておきます。
つまずきやすい点
- キャッシュプラグインが
wp-content/cacheなどに作るファイルはリリースごとに空になる。キャッシュが温まるまで負荷が上がるため、置き場所をshared/にするか、切り替え後にキャッシュを温める - プラグインによってはサーバー上のパスをオプションや設定ファイルに保存する。
$realpath_rootを使うとリリースごとにパスが変わるため、切り替え後に該当機能の動作を確認する - WP-CLI は
--path=/var/www/example/currentを指定して実行し、古いリリースのディレクトリで作業しない uploadsをリポジトリに含めていると、リンクを作る場所に実体のディレクトリができてしまう。.gitignoreで除外しておく
まとめ
- 本番のファイルを上書きせず、
releases/に組み立ててからcurrentのリンクを切り替える - 画像や秘密情報は
shared/に置き、wp-config.phpは秘密情報を読み込む形にする - Nginx では
$realpath_rootを使い、切り替えはmv -Tで原子的に行う - OPcache は CLI ではなく、cachetool や PHP-FPM の再読み込みでリセットする
- データベースは拡張→移行→縮小の順に分け、切り替え後はヘルスチェックとログ確認、問題があればリンクを戻す