コンテンツにスキップ

WordPress Media Offload 運用ガイド

WordPress の添付ファイルを Cloudflare R2 へ offload し、公開配信を cms-assets.* ドメインへ分離するための正本ドキュメントです。

1. 目的

  • WordPress origin から画像配信を切り離し、Cloudflare edge から配信する
  • WordPress 本体のディスク依存を下げる
  • Storefront / GraphQL / REST が同じ asset host を返す状態を維持する

2. 構成

バケット

環境 バケット名 配信ドメイン
staging ritsubi-wp-media-staging cms-assets-staging.ritsubi-platform.com
prod ritsubi-wp-media-prod cms-assets.ritsubi-platform.com

公開ドメイン

用途 staging prod
WordPress 管理 / API cms-staging.ritsubi-platform.com cms.ritsubi-platform.com
WordPress media 配信 cms-assets-staging.ritsubi-platform.com cms-assets.ritsubi-platform.com

必要な成立条件

  1. Secrets Manager に WORDPRESS_R2_* がある
  2. VPS 上の /opt/wordpress/.envWORDPRESS_R2_* が反映されている
  3. WordPress コンテナ内で WORDPRESS_R2_* が見えている
  4. AS3CF_SETTINGS が定義されている
  5. tantan_wordpress_s3delivery-provider=cloudflare
  6. tantan_wordpress_s3delivery-domain=cms-assets.*
  7. cloudflared が稼働し、cms(-staging) が WordPress origin を向いている

3. 実装要点

環境変数

WordPress media offload は次の環境変数を使用します。

  • WORDPRESS_R2_BUCKET_NAME
  • WORDPRESS_R2_ENDPOINT
  • WORDPRESS_R2_REGION
  • WORDPRESS_R2_ACCESS_KEY_ID
  • WORDPRESS_R2_SECRET_ACCESS_KEY
  • WORDPRESS_R2_DELIVERY_DOMAIN(任意。未設定時は WP_HOME から cms-assets.* を導出)

Storefront 側も media 配信ドメイン変更を追従させる必要があります。

  • storefront secret: VITE_PUBLIC_WORDPRESS_ASSET_BASE_URL=https://cms-assets.<domain>
  • shared secret: VITE_PUBLIC_ASSET_HOSTScms-assets.<domain> を追加
  • 反映後は storefront を再 deploy する

WordPress 側の要点

  • AS3CF_SETTINGSWORDPRESS_CONFIG_EXTRA で定義する
  • R2 endpoint / path-style は MU plugin wp-offload-media-r2.php で補正する
  • 配信 URL は delivery-provider=cloudflaredelivery-domain=cms-assets.* を使う
  • 新規 upload だけでなく、既存 attachment は明示的な再 offload が必要

画像処理

大きめ画像の upload では PHP 上限より先に ImageMagick policy が詰まりやすいため、以下をリポジトリ管理で上書きしています。

  • apps/wordpress-cms/php/uploads.ini
  • apps/wordpress-cms/php/imagemagick-policy.xml

4. 確認手順

基本確認

# env 反映
ssh ubuntu@116.80.84.4 "sudo grep '^WORDPRESS_R2' /opt/wordpress/.env"

# コンテナ env
ssh ubuntu@116.80.84.4 "sudo docker compose -f /opt/wordpress/docker-compose.yml exec -T wordpress printenv | grep '^WORDPRESS_R2'"

# AS3CF_SETTINGS
just wp-cli-vps "eval 'echo defined(\"AS3CF_SETTINGS\") ? AS3CF_SETTINGS : \"AS3CF_SETTINGS_NOT_DEFINED\";'" staging

# 配信設定
just wp-cli-vps "option get tantan_wordpress_s3" staging

# 集計
just wp-cli-vps "eval 'global \$as3cf; echo json_encode(\$as3cf->media_counts());'" staging

公開確認

curl -sS https://cms-staging.ritsubi-platform.com/wp-json/wp/v2/media?per_page=5 | jq -r '.[].source_url'
curl -sS https://cms-staging.ritsubi-platform.com/graphql -H 'content-type: application/json' \
  --data '{"query":"{ mediaItems(first: 5) { nodes { sourceUrl } } }"}'

期待値:

  • source_urlcms-assets-staging.ritsubi-platform.com / cms-assets.ritsubi-platform.com
  • media_counts().not_offloaded == 0

R2 実体確認

with-env.sh が AWS 既定 region を注入するため、R2 を AWS CLI で見るときは AWS_REGION=auto を明示します。

SECRETS_CONFIG=staging_wp ./scripts/ops/with-env.sh -- bash -lc '
  env -u AWS_REGION -u AWS_DEFAULT_REGION \
    AWS_ACCESS_KEY_ID="$WORDPRESS_R2_ACCESS_KEY_ID" \
    AWS_SECRET_ACCESS_KEY="$WORDPRESS_R2_SECRET_ACCESS_KEY" \
    AWS_REGION=auto AWS_DEFAULT_REGION=auto \
    aws s3api list-objects-v2 \
      --bucket "$WORDPRESS_R2_BUCKET_NAME" \
      --endpoint-url "$WORDPRESS_R2_ENDPOINT" \
      --output json
'

5. 既存メディアの一括 Offload

既存 attachment は新規 upload フックを通らないため、専用スクリプトで再 offload します。

# staging
just wp-cli-vps "eval-file /var/scripts/offload-existing-media.php" staging

# production は wp-cli-vps の guard が直前 DB backup を取ってから実行する
just wp-cli-vps "eval-file /var/scripts/offload-existing-media.php" production

期待値:

  1. OK <attachment_id> が並ぶ
  2. 最後に {"processed":...,"skipped":...,"failed":0}
  3. media_counts().not_offloaded == 0

6. トラブルシュート

cms(-staging)530

  • cloudflared service を確認する
  • Tunnel ingress が http://localhost:8181 を向いているか確認する
  • just update-wp-* ではなく just deploy-wp-* が必要な場合がある

AS3CF_SETTINGS_NOT_DEFINED

  • /opt/wordpress/.envWORDPRESS_R2_* が出ているか確認する
  • コンテナ env に WORDPRESS_R2_* が渡っているか確認する
  • 古い R2_* 名前空間のままになっていないか確認する

URL が cms(-staging)/wp-content/uploads/... のまま

  • delivery-providerstorage のままなら cms-assets.* は使われない
  • option get tantan_wordpress_s3cloudflare / delivery-domain を確認する
  • 既存 attachment は一括 offload スクリプトを流す

media-offload-option warning だけが出るが、公開画像は 200 で返る

  • just wp-drift-audit-vps の env 名は staging / production を使う(prod ではない)
  • まず mediaItems.sourceUrl と Storefront / WordPress GraphQL が返す代表 URL を curl -I で確認し、実害の有無を切り分ける
  • media_counts().not_offloaded == 0 かつ wp_as3cf_items.bucket が埋まっていれば、既存 offload row 自体は生きている可能性が高い
  • tantan_wordpress_s3.bucket が空でも、runtime は AS3CF_SETTINGSbucket を正として動作している場合がある。sourceUrl が 200 を返り、AS3CF_SETTINGS.bucketwp_as3cf_items.bucket が一致するなら、まず user-facing incident ではなく drift audit の false positive / live 設定 drift を疑う
  • /wp-json/ritsubi/v1/media-offload-repair404 を返す場合は、repo 上の self-heal endpoint が live WordPress へ未反映。まず WordPress code deploy/update を適用してから endpoint ベースの修復手順を使う
  • /wp-json/ritsubi/v1/media-offload-repair既定で dry-run で、candidatesCount / alreadyOffloadedCount を返すだけで副作用はない。実際に offload を再実行するには {"dryRun": false, "confirm": "RUN"} をリクエスト body に明示する必要があり、直近の本番実行から 300 秒以内は 429 ritsubi_offload_cooldown_active で拒否される。発信者を audit log に残せるよう x-ritsubi-requested-by ヘッダで実行主体(人/runbook 名)を付与することを推奨する。すべての試行は ritsubi_media_offload_repair source の error_log に JSON で残る

cms-assets.* URL なのに 404、しかし R2 にはオブジェクトが存在する

  • wp_as3cf_items.path / original_path が stale な key を向いていないか確認する
  • 代表例:
  • DB row: wp-content/uploads/2026/03/02152447/slide-01-desktop-5.jpg
  • 実在 R2 key: wp-content/uploads/2026/03/02152409/slide-01-desktop-5.jpg
  • この状態では WordPress / WPGraphQL / Storefront が stale path を source_url として返し続ける
  • apps/wordpress-cms/scripts/offload-existing-media.php は既存 Media_Library_Item row がある attachment を skip するため、ritsubi_run_media_offload_repair() や一括 offload だけでは直らない
  • wp_as3cf_items.path / original_path実在する R2 key へ補正し、その後に以下を確認する
  • curl -I https://cms-assets...200
  • https://cms(-staging).ritsubi-platform.com/graphqlmediaItems.sourceUrl が補正後 URL を返す
  • Storefront / Vendure 側は downstream cache が残るため、補正後もしばらく古い URL を返し得る。5 分 TTL を跨いで再確認する

R2 バケットが空

  • 新規 upload 自体が WordPress に作成されているかを先に見る
  • 検証後に attachment を削除すると、R2 オブジェクトも消える
  • 既存メディア未移行の状態では、公開 URL だけ見てもバケットは空のままあり得る

大きめ画像で「サーバーが画像を処理できません」

  • PHP 制限より前に ImageMagick policy が詰まることがある
  • uploads.iniimagemagick-policy.xml の両方を確認する
  • 実際に -scaled 画像と中間サイズが生成されるかで確認する

The "<name>" variable is not set. Defaulting to a blank string.

  • .env 内の $ が Docker Compose に再展開されている
  • env 生成は just/wordpress.just 側の $ エスケープを使う
  • 環境反映は staging / prod を並列実行しない