デプロイした直後に、サイトの画像だけが表示されなくなった。ファイルはちゃんと上がっているのに、なぜか出ない。
もし今その状態なら、疑うのはファイルではなく権限かもしれません。
先に結論
- 確認:画像のURLに
curlを打つ。403 なら権限、404 ならファイルが無い - 復旧:サーバー側で
chmod。ディレクトリ 755・ファイル 644 に戻す - 原因:
rsync -aが手元の権限をそのままサーバーへ複製していた - 注意:バックアップから戻しても直りません(壊れているのは中身ではなく権限)
この記事は、こんな人に向けて書いています。
rsyncでサーバーへデプロイした直後に、画像やCSSが表示されなくなった- ファイルは転送できているのに、ブラウザで見ると出てこない
- バックアップから戻したのに直らない
- 焦っているので、まず復旧して、原因は後で知りたい
【まず30秒】403 か 404 かを確認する
原因の切り分けは、これ1回で終わります。表示されない画像のURLを、そのまま貼ってください。
curl -so /dev/null -w '%{http_code}n' https://example.com/wp-content/themes/my-theme/images/logo.png
(-s と -o /dev/null は「中身は捨てて、ステータスだけ表示する」という指定です。数字が1つ出れば正常に動いています。)
返ってきた3桁の数字で分かれます。
| 結果 | 意味 | この記事が効くか |
|---|---|---|
| 403 | ファイルはあるが、サーバーが「読ませない」と言っている | 効きます。このまま読み進めてください |
| 404 | そもそもファイルが無い | 別の原因です(転送漏れ・パスの間違い) |
| 200 | 正常に配信できている | この記事の対象外。ブラウザのキャッシュ、HTML側のパス、CSS の指定を先に疑ってください |
ターミナルが手元にないときは、ブラウザでも確認できます。画像を右クリックして「新しいタブで開く」か、開発者ツールの Network タブを開いてリロードすると、同じステータスが見られます。
404 だった方へ。 この記事の内容は当てはまりません。転送先のパスが合っているか、rsync の対象から漏れていないかを先に確認してください。
【復旧する】サーバー側で権限を戻す
403 なら、サーバー側で権限を戻せばその場で直ります。SSH でログインして、次を実行してください。
⚠ パスの指定範囲に注意
chmod は指定したパスの中身すべてに効きます。public_html の直下など広い範囲を指定すると、サイト全体の権限を書き換えてしまいます。特に wp-config.php を含む範囲に流すと、本来 600 にしておきたいファイルまで 644 に緩みます。テーマのディレクトリまでに限定してください。
find /home/USER/example.com/public_html/wp-content/themes/my-theme -type d -exec chmod 755 {} +
find /home/USER/example.com/public_html/wp-content/themes/my-theme -type f -exec chmod 644 {} +
パスは自分の環境に置き換えてください。やっていることは2行だけです。
- 1行目:ディレクトリを全部
755にする - 2行目:ファイルを全部
644にする
ファイルを「誰がどう扱えるか」の設定です。3桁はそれぞれ持ち主/グループ/その他全員を表しています。Web サイトの場合、ページを配信するのは自分ではなくサーバー上の別のプロセスなので、その他全員にも「読む」権限が要ります。ディレクトリ 755・ファイル 644 が Web の標準的な値です。
実行したら、さっきの curl をもう一度打ってみてください。200 が返れば復旧完了です。
バックアップから戻しても直りません
先に言っておきます。この障害は、バックアップからの復元では直りません。
焦っているとき、まず思いつくのが「バックアップから戻す」だと思います。自分もそうでした。でも戻らないんです。
理由はシンプルで、壊れているのがファイルの中身ではないからです。バックアップが復元するのはファイルの内容であって、権限は別のレイヤーの情報。中身が完全に元どおりでも、権限が 700 のままなら、やはり配信できません。
だからやるべきは復元ではなく chmod です。上の手順で直ります。
(もちろん、バックアップ自体は必要です。UpdraftPlus で Google Drive へバックアップを取る方法も別記事に書いています。今回の障害に効かないというだけで、いらないという話ではありません。)
【何が起きていたのか】rsync -a は権限も一緒に運ぶ
ここから先は原因の話です。復旧が終わってから読んでもらって大丈夫です。
犯人は、rsync でよく使われる -a(アーカイブモード) でした。

-a は便利なショートハンドで、複数のオプションをまとめたものです。手元の man rsync で確認するとこう書かれています。
-a, --archive
Shorthand for -Dgloprt.
この中の p が「権限(パーミッション)を保持する」 という指定です。つまり -a を使うと、手元のファイルやフォルダの権限が、そのままサーバー側へ複製されます。
これの何が問題かというと、手元のフォルダは「自分だけが読み書きできればいい」状態になっていることがあるからです。自分の場合、テーマのディレクトリが 700 になっていました。手元では何も困りません。でもそれがサーバーへ運ばれると、サーバー上の配信プロセスがファイルを読めなくなり、403 になります。
転送そのものは成功しています。ファイルもちゃんと届いている。届いた先で読めなくなっているだけなので、転送ログを見ても異常は見つかりません。
なぜ気づきにくいのか
この障害のいちばん厄介なところは、壊れ方が中途半端に見えることです。

ページ自体は普通に表示されます。真っ白にならないんです。多くのレンタルサーバーのように PHP が所有者の権限で動く構成では、HTML の生成は問題なくできてしまいます。「サイトが落ちた」という分かりやすい状態にはなりません。
さらに紛らわしいことが重なりました。キャッシュ系のプラグインが CSS や JS を別の場所から配信する構成になっていると、そちらは無事なままです。結果として、画像だけが欠けているように見えます。
そうなると、まず疑うのは画像ですよね。自分もメディアライブラリを開いたり、パスを確認したり。原因は権限なのに、画像を調べている。これでは見つかりません。
(この見え方はプラグインの構成によって変わります。CSS も一緒に崩れることもあれば、画像だけのこともあります。症状の出方より、curl の結果で判断するほうが確実です。)
二度と起こさないための3点セット
原因が分かったので、あとは仕組みで防ぎます。デプロイのスクリプトに次の3つを入れておけば済む話です。
① rsync に --no-perms を付ける
手元の権限をサーバーへ持ち込まない指定です。これで伝播そのものが止まります。
rsync -avz --no-perms --exclude='.DS_Store' --exclude='.git'
-e "ssh -i ~/.ssh/deploy_key -p 10022"
./my-theme/ user@svXXXX.xserver.jp:/home/USER/example.com/public_html/wp-content/themes/my-theme/
② 送ったあと、サーバー側で権限を正規化する
--no-perms だけでは足りません。 すでに壊れている権限は直らないからです。あれは「これ以上持ち込まない」だけで、過去に運ばれたものはそのまま。
なので、デプロイのたびに権限を揃える処理を足します。復旧のときに使ったコマンドと同じものです。
ssh -i ~/.ssh/deploy_key -p 10022 user@svXXXX.xserver.jp
"find '/home/USER/example.com/public_html/wp-content/themes/my-theme/' -type d -exec chmod 755 {} + ;
find '/home/USER/example.com/public_html/wp-content/themes/my-theme/' -type f -exec chmod 644 {} +"
③ 公開URLで、実際に配信できているか確かめる
ここがいちばん大事かもしれません。転送が成功したことと、配信できることは別だからです。
code=$(curl -so /dev/null -w '%{http_code}' "https://example.com/wp-content/themes/my-theme/style.css" || echo 000)
[ "$code" = "200" ] && echo "✅ 配信OK($code)" || echo "⚠️ 静的ファイルが HTTP $code です。権限かパスを確認してください"
デプロイの最後にこれを走らせておけば、次に同じことが起きた瞬間に気づけます。人間が毎回気をつけるより、こちらのほうが確実ですね。
なぜ --chmod で済ませないのか
rsync には --chmod というオプションがあり、転送時に権限を指定できます。1行で済むならそのほうが良さそうに見えますよね。
ただ、今回のやり方には合いませんでした。理由は3つあります。
--no-permsと併用できない。 man を読むと--chmodは「--permsが指定されているときにのみ効果がある」とあります。①で--no-permsを付けている以上、そもそも効きません- 転送するファイルにしか効かない。 すでにサーバー側にある壊れた権限は直りません(②が必要な理由と同じです)
- 8進数での指定が通らない環境がある。 自分の環境(macOS 標準の openrsync)では
--chmod=D755,F644も--chmod=755もinvalid argumentで弾かれました。Dg+wのような相対指定なら通ります
なので、転送オプションではなく、送ったあとの chmod で確定させる形にしました。
【教訓】検証は「中身」だけでなく「権限」も見る
この件から持ち帰ったのは、デプロイの確認の仕方でした。
自分の「成功」の判断基準は、サイトを開いて画像がちゃんと表示されることでした。今回もそれで気づけています。ただ、気づけたのはたまたま開いたからです。
しかも壊れたのは、手を入れていたところとは別の場所でした。改修していない、ずっと前から載っていた画像が、まとめて表示されなくなった。「自分が触った範囲だけ確認すればいい」という前提が崩れるわけです。
毎回すべてのページを目視するのは現実的ではありません。だから確認そのものをスクリプトに持たせました(3点セットの③)。人間の注意力に頼る運用は、いつか抜けます。
もうひとつ。rsync をドライラン(--dry-run)で流すと、ディレクトリの行も出てきます。以前の自分はそこをノイズだと思って読み飛ばしていました。権限が伝播する兆候は、まさにその行に出ます。
そして一番の教訓は、「安全」と言う前に、確認した軸とそうでない軸を区別することです。転送は確認した。配信は確認していない。それなら「転送は成功した」と言うべきで、「デプロイは成功した」と言ってはいけなかった。
まとめ
- 切り分けは
curl一発。403なら権限、404ならファイルが無い。症状の見え方ではなくステータスで判断する - 復旧はサーバー側で
chmod。 ディレクトリ 755・ファイル 644 に戻せばその場で直る - バックアップ復元では直らない。 壊れているのは中身ではなく権限だから
- 原因は
rsync -aに含まれるp。 手元の権限がそのままサーバーへ複製される(-aの中身は環境により-Dgloprt/-rlptgoDと表記が違いますが、どちらもpを含みます) - 再発防止は3点セット。
--no-permsで持ち込まない/送ったあとchmodで揃える/公開URLで配信を実測する
「デプロイは成功したのにサイトが壊れている」ときは、原因が転送の外側にあります。ログを何度見返しても異常は出ません。届いた先で何が起きているかに目を移すと、案外あっさり片付きます。
なお、ここで出てきた deploy.sh の全文と、git push に連動させる設定は別記事にまとめています。エックスサーバーで自動デプロイを組みたい方は、こちらもあわせてどうぞ。
WordPress テーマの更新を毎回 FTP でアップロードするのをやめたくて、GitHub Actions で自動デ…
WordPress サイトの管理運用や、デプロイまわりの自動化・改善のお手伝いもしています。「今の運用のどこを直せばいいか分からない」くらいの段階でも大丈夫です。お問い合わせから気軽にどうぞ。相談だけでもかまいません。

