2026年5月20日

2026年5月20日

rsyncでリモートバックアップする方法|末尾スラッシュ・–deleteの注意点と世代管理

rsyncを使えば、SSH経由で別のサーバーへファイルをコピーし、2回目以降は変更分だけを送る効率のよいバックアップを組めます。ただし、送信元の末尾に / を付けるかどうかでコピー結果が変わり、--delete を付けるとバックアップ先のファイルが消えることもあります。この記事では、その2つの落とし穴を避けながら、--dry-run で確認してから実行する手順、SSH鍵の制限、--link-dest を使った世代バックアップ、cronでの自動化までを説明します。

基本の形とよく使うオプション

rsyncの書式は rsync [オプション] 送信元 送信先 です。リモートは ユーザー名@ホスト:パス の形で書き、既定でSSHが使われます。

rsync -avh --dry-run /var/www/html/ backup@backup.example.com:/backup/web/html/

よく使うオプションは次のとおりです。

  • -a(アーカイブ):ディレクトリを再帰的にたどり、パーミッション・タイムスタンプ・シンボリックリンク・所有者などを保ったままコピーします。バックアップではほぼ常に付けます
  • -v:処理したファイル名を表示します。-h はサイズを読みやすい単位で表示します
  • -z:転送時に圧縮します。回線が遅いときに有効ですが、画像や圧縮済みファイルが中心なら効果は小さく、CPUを使うだけになることもあります
  • -n / --dry-run:実際には何も変更せず、何が起きるかだけを表示します
  • --exclude:除外するパターンを指定します
  • -e:使うリモートシェルを指定します。SSHのポートや鍵を変えるときに使います

-a は所有者やグループも保とうとしますが、受け取り側がroot権限で動いていない場合、所有者は接続ユーザーに置き換わります。復元時に所有者まで戻したいなら、受け取り側でroot権限を使うか、tarで固めてから送る方法を検討します。

末尾スラッシュの意味を押さえる

rsyncで最初につまずくのが、送信元パスの末尾スラッシュです。

rsync -a /var/www/html  /backup/   # → /backup/html/ が作られ、その中にコピー
rsync -a /var/www/html/ /backup/   # → /backup/ の直下に html の「中身」がコピー

送信元の末尾に / があると「このディレクトリの中身」、ないと「このディレクトリそのもの」という意味になります。送信先の末尾スラッシュは、ほとんどの場合結果に影響しません。

この違いは、--delete と組み合わせたときに深刻になります。例えば /backup/ に他のバックアップも置いている状態で rsync -a --delete /var/www/html/ /backup/ を実行すると、/backup/ が html の中身と同じ状態にされ、他のバックアップが削除されます。送信元と送信先は「同じ階層どうし」がそろうように、両方とも末尾スラッシュ付きで、送信先も最下層のディレクトリまで書く習慣をつけると間違いが減ります。

--deleteの危険性と安全な使い方

--delete は、送信元にないファイルを送信先から削除するオプションです。送信元で消したファイルがバックアップにいつまでも残る問題は解決しますが、次のような状況では、バックアップを壊す方向に働きます。

  • 送信元のディスクがマウントされておらず、空のディレクトリを指していた
  • スクリプトの変数が空になり、意図しないパスが送信元になった
  • ランサムウェアや誤操作で送信元のファイルが消えたり、暗号化されたりした

最後のケースでは、壊れた状態がそのままバックアップに反映されます。1つの保存先を --delete で同期するだけの構成は「ミラー」であって、過去の状態に戻せるバックアップではない、と考えてください。

--delete を使うときは、次の安全策を組み合わせます。

# 1. まず dry-run で、削除対象(deleting と表示される行)を確認する
rsync -avh --delete --dry-run /var/www/html/ backup@backup.example.com:/backup/web/html/

# 2. 削除件数に上限を設け、消えるファイルは退避する
rsync -avh --delete --max-delete=100 \
  --backup --backup-dir=/backup/web/deleted/$(date +%F) \
  /var/www/html/ backup@backup.example.com:/backup/web/html/

--max-delete=100 は、削除が100件に達すると以降の削除をスキップし、最後に警告を出して終了コード25で終わります。--backup-dir を指定すると、削除・上書きされるファイルが指定したディレクトリへ移されます。相対パスで書くと送信先ディレクトリからの相対位置になるため、ここではフルパスで指定しています。

スクリプトで使う場合は、実行前に送信元が空でないか、マウントされているかを確認する処理も入れておくと安心です。

SSH鍵を用意し、権限を絞る

自動実行には、パスフレーズなしのSSH鍵が必要になります。その鍵が漏れたときに被害を広げないよう、バックアップ専用の鍵を作り、できることを制限します。

ssh-keygen -t ed25519 -f ~/.ssh/backup_ed25519 -N "" -C "rsync-backup"

ポートや鍵を指定して接続するには -e を使います。

rsync -avh -e "ssh -i ~/.ssh/backup_ed25519 -p 2222" \
  /var/www/html/ backup@backup.example.com:/backup/web/html/

バックアップを置く側では、authorized_keys でこの鍵の用途を絞れます。rsyncには、特定のディレクトリ以下のrsyncしか許可しない rrsync というスクリプトが付属しています(配置場所はディストリビューションや版によって異なるため、which rrsync などで確認してください)。

command="/usr/bin/rrsync /backup/web",restrict ssh-ed25519 AAAA...(公開鍵) rsync-backup

restrict はポート転送やシェルの割り当てなどをまとめて禁止するOpenSSHのオプションです。これで、この鍵ではシェルにログインできず、/backup/web 以下へのrsyncしかできなくなります。rrsyncを使うと、接続する側が指定するパスは制限したディレクトリからの相対パスになるため、送信先は backup@backup.example.com:html/ のように書き換えます。

さらに安全性を高めるなら、「Webサーバーからバックアップサーバーへ送る(push)」のではなく、「バックアップサーバーからWebサーバーを取りに行く(pull)」構成にします。pushでは、Webサーバーを乗っ取られると、その鍵でバックアップ先を書き換えられます。pullなら、Webサーバー側にはバックアップサーバーへの鍵が存在しません。pull側の鍵には rrsync -ro(読み取り専用)を組み合わせられます。

--link-destで世代バックアップを作る

--delete のミラーでは過去に戻れない問題は、--link-dest で解決できます。前回のバックアップと内容が同じファイルはハードリンクで共有し、変わったファイルだけを新しく保存するため、毎日フルバックアップのように見える日付別ディレクトリを、差分程度の容量で持てます。

#!/bin/bash
# バックアップサーバー側で実行する pull 型の例
set -euo pipefail

SRC="web@www.example.com:/var/www/html/"
DEST_BASE=/backup/web
TODAY=$(date +%F)
LATEST="$DEST_BASE/latest"

rsync -a --delete \
  -e "ssh -i /root/.ssh/pull_ed25519" \
  --link-dest="$LATEST" \
  --exclude "wp-content/cache/" \
  "$SRC" "$DEST_BASE/$TODAY/"

ln -sfn "$DEST_BASE/$TODAY" "$LATEST"

# 30日より古い世代を削除
find "$DEST_BASE" -mindepth 1 -maxdepth 1 -type d -name "20*" -mtime +30 -exec rm -rf {} +

ここでの --delete は、新しく作る日付ディレクトリの中だけに作用するため、過去の世代は消えません。初回は latest が存在しないため警告が出ますが、通常のフルコピーとして処理されます。--link-dest に相対パスを渡すと送信先ディレクトリからの相対位置と解釈されるため、フルパスで指定しています。

古い世代の削除は rm -rf を使うので、DEST_BASE が空にならないこと、-name "20*" で日付ディレクトリだけを対象にしていることを、最初に -exec の代わりに -print を付けて確認してください。

データベースは別に扱う

稼働中のMySQLのデータディレクトリ(/var/lib/mysql)をrsyncでコピーしても、書き込み途中の状態が混ざり、復元できないことがあります。データベースは mysqldump などでダンプファイルを作り、そのファイルをrsyncで送ります。WordPressなら、ダンプをWebサーバー上の公開ディレクトリの外に出力しておき、pull側がそのディレクトリも取りに行く形が扱いやすいです。

cronで自動化し、結果を確認する

cronに登録する際は、前回の実行が終わる前に次が始まらないよう flock を使い、ログを残します。

# crontab -e(バックアップサーバー側)
15 2 * * * /usr/bin/flock -n /tmp/rsync-backup.lock /usr/local/bin/pull-backup.sh >> /var/log/rsync-backup.log 2>&1

rsyncの終了コードは、0が正常終了です。23は一部のファイルが転送できなかった場合(権限不足など)、24は転送中に送信元のファイルが消えた場合で、ログやキャッシュのように頻繁に入れ替わるファイルがあると24はよく出ます。終了コードを見て、0と24以外ならメールや監視ツールに通知する仕組みにしておくと、失敗に気づかないまま日数が過ぎる事態を防げます。

最後に、バックアップが本当に使えるかを定期的に確かめます。日付ディレクトリから数ファイルを取り出して中身を見る、テスト環境に丸ごと戻してサイトが表示されるかを試す、といった復元テストを月に一度でも行うと、いざというときに慌てずに済みます。(2026年10月現在の主要なLinuxディストリビューションに含まれるrsync 3.2系以降を前提にしています。)

まとめ

  • 送信元の末尾 / は「中身」、なしは「ディレクトリそのもの」。両側とも末尾スラッシュ付きでそろえると間違いが減る
  • --delete は送信先を消す操作。--dry-run で確認し、--max-delete と --backup-dir で被害を抑える
  • 自動化用の鍵は専用に作り、rrsync と restrict で用途を絞る。可能ならpull型にする
  • 過去に戻せるバックアップには --link-dest で日付別の世代を作る
  • データベースはダンプしてから送り、終了コードの監視と復元テストを続ける

お気軽にご相談ください

お見積りへ お問い合わせへ
▲