2026年6月23日
2026年6月23日
ShipItでWordPressのゼロダウンタイムデプロイを実現する方法
ShipIt(Node.js 製のデプロイツール shipit-cli と shipit-deploy)を使うと、WordPress のファイルを「リリースごとの別ディレクトリに配置し、最後にシンボリックリンクを切り替える」方式で更新でき、更新中にサイトが中途半端な状態で表示される時間をほぼなくせます。この記事では、1台のサーバーで動く WordPress を前提に、ディレクトリ設計、shipitfile.js の書き方、アップロードファイルや wp-config.php の共有、PHP-FPM と OPcache の注意点、ロールバックまでを順に説明します。
ゼロダウンタイムデプロイの仕組み
FTP や git pull で本番ディレクトリを直接書き換えると、ファイルが入れ替わっている数秒〜数十秒のあいだ、新旧のファイルが混ざった状態でリクエストが処理されます。テーマの関数ファイルだけ新しく、読み込まれる側が古いといった状態になると、致命的なエラーで画面が真っ白になることがあります。
ShipIt の shipit-deploy は Capistrano と同じ考え方で、次のような構成を作ります。
/var/www/example/
├── current -> releases/20261018093000 # Web サーバーのドキュメントルート
├── releases/
│ ├── 20261017120000/
│ └── 20261018093000/
└── shared/ # リリースをまたいで残すもの(自分で用意)
├── wp-config.php
└── uploads/
新しいリリースは releases/ の下に丸ごと転送され、準備が終わった時点で current のリンク先だけを切り替えます。切り替えは一瞬なので、利用者からは「ある瞬間を境に新しい版になった」ように見えます。古いリリースは指定した世代数だけ残るため、問題が出たらリンクを戻すだけで以前の状態に戻せます。
事前に確認すること
ShipIt の現状
shipit-cli / shipit-deploy は GitHub で公開されているオープンソースですが、更新頻度は高くありません(2026年10月現在)。採用前にリポジトリの最終リリース日、Issue の状況、手元の Node.js で動くかを確認してください。PHP で書かれた Deployer など、同じリリースディレクトリ方式のツールもあるので、チームの慣れている言語で選ぶのも一つの考え方です。
前提条件
- ローカル(または CI)に Node.js と npm、サーバーに SSH と rsync が入っていること
- 公開鍵認証で SSH ログインできるデプロイ用ユーザーがいること
- WordPress 一式(または wp-content 配下のテーマ・プラグイン)を Git で管理していること
- wp-config.php とアップロードファイルは Git に含めないこと
最初は本番ではなくステージング環境で試し、作業前にファイルとデータベースのバックアップを取ってください。
サーバー側の準備
まず共有ディレクトリを作り、既存の wp-config.php とアップロードファイルを移します。
sudo mkdir -p /var/www/example/{releases,shared}
sudo chown -R deploy:www-data /var/www/example
# 既存サイトから移す例(パスは環境に合わせて変更)
cp /var/www/html/wp-config.php /var/www/example/shared/wp-config.php
rsync -a /var/www/html/wp-content/uploads/ /var/www/example/shared/uploads/
この方式では、デプロイのたびに新しいディレクトリへコードが置き換わります。そのため管理画面からプラグインやテーマ、コアを更新すると、その変更は次のデプロイで消えます。更新は Git 側で行う運用に切り替え、wp-config.php に次の定数を入れて管理画面からのファイル変更を止めておくと事故を防げます。
// shared/wp-config.php に追記(コードの更新は Git 経由で行う運用にする)
define( 'DISALLOW_FILE_MODS', true );
なお、wp-config.php をシンボリックリンクで読み込ませると、その中の __DIR__ はリンク先の shared/ を指します。wp-config.php の中で相対的に別ファイルを読み込んでいる場合はパスを見直してください。
shipitfile.js を書く
プロジェクトのルートでパッケージを入れます。
npm install --save-dev shipit-cli shipit-deploy
次に shipitfile.js を作ります。ホスト名やパスはダミーなので置き換えてください。
// shipitfile.js
const workspace = '/tmp/shipit-example-wp';
module.exports = (shipit) => {
require('shipit-deploy')(shipit);
const deployTo = '/var/www/example';
const shared = `${deployTo}/shared`;
shipit.initConfig({
default: {
workspace, // 毎回作り直される作業ディレクトリ(リポジトリ自身を指定しない)
deployTo,
repositoryUrl: 'git@github.com:your-org/your-wp-site.git',
branch: 'main',
ignores: ['.git', 'node_modules', '.github'],
keepReleases: 5, // 残すリリースの世代数
shallowClone: true,
},
staging: {
servers: 'deploy@staging.example.com',
},
production: {
servers: 'deploy@www.example.com',
},
});
// 取得後・転送前: ローカルの作業ディレクトリでビルドする
shipit.on('fetched', () => shipit.start('build'));
// 転送後・切り替え前: 共有ファイルをリンクする
shipit.on('updated', () => shipit.start('link-shared'));
// 切り替え後: PHP-FPM を再読み込みする
shipit.on('published', () => shipit.start('reload-php'));
shipit.blTask('build', async () => {
await shipit.local('composer install --no-dev --optimize-autoloader', { cwd: workspace });
await shipit.local('npm ci && npm run build', { cwd: `${workspace}/wp-content/themes/your-theme` });
});
shipit.blTask('link-shared', async () => {
const release = shipit.releasePath;
await shipit.remote(`ln -nfs ${shared}/wp-config.php ${release}/wp-config.php`);
await shipit.remote(`rm -rf ${release}/wp-content/uploads`);
await shipit.remote(`ln -nfs ${shared}/uploads ${release}/wp-content/uploads`);
});
shipit.blTask('reload-php', async () => {
// サービス名は PHP のバージョンやディストリビューションで異なる
await shipit.remote('sudo systemctl reload php8.3-fpm');
});
};
ポイントは、共有ファイルのリンクを「切り替え前」に済ませることです。updated の時点では新しいリリースは転送済みですが、current はまだ古いリリースを指しています。ここで wp-config.php やアップロードのリンクを張っておけば、切り替えた瞬間から新しいリリースが正常に動きます。順序を逆にすると、切り替え直後の一瞬だけデータベース設定が無い状態になり、「データベース接続確立エラー」が表示されることがあります。
ビルドは本番サーバーではなく、ローカルまたは CI の作業ディレクトリで行います。サーバーに Node.js を入れずに済み、ビルド失敗時は転送前に止まるので本番に影響しません。イベント名やオプション名は shipit-deploy のバージョンで変わる可能性があるため、利用する版の README と照らし合わせてください。sudo systemctl reload を使う場合は、デプロイユーザーにそのコマンドだけを許可する sudoers 設定が必要です。
Web サーバーと OPcache の設定
シンボリックリンク方式でつまずきやすいのが、PHP 側のキャッシュです。PHP は realpath キャッシュと OPcache を持っており、current のリンク先を変えても、しばらく古いリリースのファイルを使い続けることがあります。「デプロイしたのに反映されない」「一部だけ古い」という症状の多くはこれが原因です。
Nginx では、PHP に渡すパスをリンク解決後の実パスにします。
server {
root /var/www/example/current;
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;
}
}
$realpath_root を使うと、リクエストごとに current が解決され、PHP には releases/20261018093000/index.php のような実パスが渡ります。リリースごとにパスが変わるので OPcache が古いファイルを返すことはなくなります。古いリリース分のキャッシュはメモリに残るため、上の例のように切り替え後に PHP-FPM を reload して整理します。reload は多くの環境で処理中のリクエストを終えてからワーカーを入れ替えますが、挙動は process_control_timeout などの設定に左右されるので、ステージングで負荷をかけながら確認すると安心です。
デプロイとロールバックの実行
# ステージングで確認してから本番へ
npx shipit staging deploy
npx shipit production deploy
# 直前のリリースへ戻す
npx shipit production rollback
デプロイ後は次の点を確認します。
ls -l /var/www/example/currentでリンク先が新しいリリースになっているか- トップページ、ログイン画面、画像が表示されるか(アップロードのリンク漏れの確認)
- エラーログ(PHP-FPM と Nginx)に新しいエラーが出ていないか
ロールバックはリンクを前のリリースに戻すだけなので数秒で終わります。ただし、戻るのはファイルだけでデータベースは戻りません。プラグインの更新でテーブル構造が変わった後に古いコードへ戻すと、整合しなくなる場合があります。データベースに影響する更新を含むデプロイでは、直前に wp db export などでダンプを取り、戻す手順を決めてから実行してください。
つまずきやすい点
- キャッシュ系プラグインの書き込み先: ページキャッシュを wp-content 配下に書くプラグインは、リリースが変わるとキャッシュが空になります。動作上は問題ありませんが、デプロイ直後は負荷が一時的に上がります。advanced-cache.php などのドロップインファイルが自動生成される場合は、Git に含めるか共有ディレクトリでリンクするかを決めておきます。
- ディスク容量: リリースは丸ごとコピーされるため、
keepReleasesの数だけ容量を使います。vendor や node_modules を含めない、ビルド成果物だけを送るなどで小さくできます。 - パーミッション: アップロードは Web サーバーの実行ユーザーが書き込めないと失敗します。共有ディレクトリの所有者とグループを確認してください。
まとめ
- ShipIt はリリースごとのディレクトリにコードを置き、
currentのリンク切り替えで公開するため、更新中に新旧ファイルが混ざる時間をなくせます。 - wp-config.php とアップロードは
shared/に置き、切り替え前のupdatedイベントでリンクします。 - 管理画面からのファイル更新は止め、更新は Git とデプロイで行います。
- Nginx では
$realpath_rootを使い、切り替え後に PHP-FPM を reload して OPcache の古いファイルを避けます。 - ロールバックで戻るのはファイルだけです。データベースはバックアップで備えます。
- ShipIt 自体の更新頻度は高くないため、採用前にリポジトリの状況を確認してください。