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 |
必要な成立条件¶
- Secrets Manager に
WORDPRESS_R2_*がある - VPS 上の
/opt/wordpress/.envにWORDPRESS_R2_*が反映されている - WordPress コンテナ内で
WORDPRESS_R2_*が見えている AS3CF_SETTINGSが定義されているtantan_wordpress_s3のdelivery-provider=cloudflaretantan_wordpress_s3のdelivery-domain=cms-assets.*cloudflaredが稼働し、cms(-staging)が WordPress origin を向いている
3. 実装要点¶
環境変数¶
WordPress media offload は次の環境変数を使用します。
WORDPRESS_R2_BUCKET_NAMEWORDPRESS_R2_ENDPOINTWORDPRESS_R2_REGIONWORDPRESS_R2_ACCESS_KEY_IDWORDPRESS_R2_SECRET_ACCESS_KEYWORDPRESS_R2_DELIVERY_DOMAIN(任意。未設定時はWP_HOMEからcms-assets.*を導出)
Storefront 側も media 配信ドメイン変更を追従させる必要があります。
storefrontsecret:VITE_PUBLIC_WORDPRESS_ASSET_BASE_URL=https://cms-assets.<domain>sharedsecret:VITE_PUBLIC_ASSET_HOSTSにcms-assets.<domain>を追加- 反映後は storefront を再 deploy する
WordPress 側の要点¶
AS3CF_SETTINGSはWORDPRESS_CONFIG_EXTRAで定義する- R2 endpoint / path-style は MU plugin
wp-offload-media-r2.phpで補正する - 配信 URL は
delivery-provider=cloudflareとdelivery-domain=cms-assets.*を使う - 新規 upload だけでなく、既存 attachment は明示的な再 offload が必要
画像処理¶
大きめ画像の upload では PHP 上限より先に ImageMagick policy が詰まりやすいため、以下をリポジトリ管理で上書きしています。
apps/wordpress-cms/php/uploads.iniapps/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_urlがcms-assets-staging.ritsubi-platform.com/cms-assets.ritsubi-platform.commedia_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
期待値:
OK <attachment_id>が並ぶ- 最後に
{"processed":...,"skipped":...,"failed":0} media_counts().not_offloaded == 0
6. トラブルシュート¶
cms(-staging) が 530¶
cloudflaredservice を確認する- Tunnel ingress が
http://localhost:8181を向いているか確認する just update-wp-*ではなくjust deploy-wp-*が必要な場合がある
AS3CF_SETTINGS_NOT_DEFINED¶
/opt/wordpress/.envにWORDPRESS_R2_*が出ているか確認する- コンテナ env に
WORDPRESS_R2_*が渡っているか確認する - 古い
R2_*名前空間のままになっていないか確認する
URL が cms(-staging)/wp-content/uploads/... のまま¶
delivery-providerがstorageのままならcms-assets.*は使われないoption get tantan_wordpress_s3でcloudflare/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_SETTINGSのbucketを正として動作している場合がある。sourceUrlが 200 を返り、AS3CF_SETTINGS.bucketとwp_as3cf_items.bucketが一致するなら、まず user-facing incident ではなく drift audit の false positive / live 設定 drift を疑う/wp-json/ritsubi/v1/media-offload-repairが404を返す場合は、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_repairsource の 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_Itemrow がある attachment を skip するため、ritsubi_run_media_offload_repair()や一括 offload だけでは直らないwp_as3cf_items.path/original_pathを 実在する R2 key へ補正し、その後に以下を確認するcurl -I https://cms-assets...が200https://cms(-staging).ritsubi-platform.com/graphqlのmediaItems.sourceUrlが補正後 URL を返す- Storefront / Vendure 側は downstream cache が残るため、補正後もしばらく古い URL を返し得る。5 分 TTL を跨いで再確認する
R2 バケットが空¶
- 新規 upload 自体が WordPress に作成されているかを先に見る
- 検証後に attachment を削除すると、R2 オブジェクトも消える
- 既存メディア未移行の状態では、公開 URL だけ見てもバケットは空のままあり得る
大きめ画像で「サーバーが画像を処理できません」¶
- PHP 制限より前に ImageMagick policy が詰まることがある
uploads.iniとimagemagick-policy.xmlの両方を確認する- 実際に
-scaled画像と中間サイズが生成されるかで確認する
The "<name>" variable is not set. Defaulting to a blank string.¶
.env内の$が Docker Compose に再展開されている- env 生成は
just/wordpress.just側の$エスケープを使う - 環境反映は staging / prod を並列実行しない