rsync でデプロイしたら本番の画像だけ消えた|`-a` オプションがパーミッションを上書きする落とし穴

デプロイした直後に、サイトの画像だけが表示されなくなった。ファイルはちゃんと上がっているのに、なぜか出ない。

もし今その状態なら、疑うのはファイルではなく権限かもしれません。

先に結論

  • 確認:画像の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 にする
755 とか 644 って何?
ファイルを「誰がどう扱えるか」の設定です。3桁はそれぞれ持ち主/グループ/その他全員を表しています。Web サイトの場合、ページを配信するのは自分ではなくサーバー上の別のプロセスなので、その他全員にも「読む」権限が要ります。ディレクトリ 755・ファイル 644 が Web の標準的な値です。

実行したら、さっきの curl をもう一度打ってみてください。200 が返れば復旧完了です。

バックアップから戻しても直りません

先に言っておきます。この障害は、バックアップからの復元では直りません

焦っているとき、まず思いつくのが「バックアップから戻す」だと思います。自分もそうでした。でも戻らないんです。

理由はシンプルで、壊れているのがファイルの中身ではないからです。バックアップが復元するのはファイルの内容であって、権限は別のレイヤーの情報。中身が完全に元どおりでも、権限が 700 のままなら、やはり配信できません。

だからやるべきは復元ではなく chmod です。上の手順で直ります。

(もちろん、バックアップ自体は必要です。UpdraftPlus で Google Drive へバックアップを取る方法も別記事に書いています。今回の障害に効かないというだけで、いらないという話ではありません。)

【何が起きていたのか】rsync -a は権限も一緒に運ぶ

ここから先は原因の話です。復旧が終わってから読んでもらって大丈夫です。

犯人は、rsync でよく使われる -a(アーカイブモード) でした。

rsyncのアーカイブモードでローカルのディレクトリ権限700がそのまま本番へ複製される仕組みの図

-a は便利なショートハンドで、複数のオプションをまとめたものです。手元の man rsync で確認するとこう書かれています。

-a, --archive
      Shorthand for -Dgloprt.

この中の p が「権限(パーミッション)を保持する」 という指定です。つまり -a を使うと、手元のファイルやフォルダの権限が、そのままサーバー側へ複製されます

これの何が問題かというと、手元のフォルダは「自分だけが読み書きできればいい」状態になっていることがあるからです。自分の場合、テーマのディレクトリが 700 になっていました。手元では何も困りません。でもそれがサーバーへ運ばれると、サーバー上の配信プロセスがファイルを読めなくなり、403 になります

転送そのものは成功しています。ファイルもちゃんと届いている。届いた先で読めなくなっているだけなので、転送ログを見ても異常は見つかりません。

なぜ気づきにくいのか

この障害のいちばん厄介なところは、壊れ方が中途半端に見えることです。

ページのテキストは表示されるのに画像だけが読み込めていないWebページの再現図

ページ自体は普通に表示されます。真っ白にならないんです。多くのレンタルサーバーのように 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つあります。

  1. --no-perms と併用できない。 man を読むと --chmod は「--perms が指定されているときにのみ効果がある」とあります。①で --no-perms を付けている以上、そもそも効きません
  2. 転送するファイルにしか効かない。 すでにサーバー側にある壊れた権限は直りません(②が必要な理由と同じです)
  3. 8進数での指定が通らない環境がある。 自分の環境(macOS 標準の openrsync)では --chmod=D755,F644--chmod=755invalid 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 サイトの管理運用や、デプロイまわりの自動化・改善のお手伝いもしています。「今の運用のどこを直せばいいか分からない」くらいの段階でも大丈夫です。お問い合わせから気軽にどうぞ。相談だけでもかまいません。

XIARD DESIGN

初回相談は無料です。Webサイトの管理運用・保守、AI活用支援のご相談を承っています。オンラインで全国対応、3営業日以内…