先に要点
DBを戻したら記事は表示された。でも添付を開くとエラーになる。VPSのバックアップでは、こうした「一部だけ戻った状態」を見落とさないことが大切です。アプリはDBだけで動いているわけではなく、ファイル、設定、暗号鍵、実行環境も使うからです。
まず小規模なWebアプリで残すものを整理し、その後でMySQLと添付ファイルを実際に復元した例を紹介します。自分の構成に当てはめるときは、表の保存対象を埋め、最後の復旧確認まで一度通してみてください。
VPSで残すものは「データ」と「再現に必要な情報」
VPSが壊れたとき、OSやアプリコードは再配置できても、利用者が登録したデータは作り直せません。一方、データが残っていても、対応する設定やコードが分からなければ起動できません。この二つを組み合わせてバックアップします。
| 対象 | 小規模Webアプリでの例 | 残し忘れると起きること |
|---|---|---|
| DB | 記事・利用者・添付の参照先、テーブル定義。利用していればトリガーやルーチンも | 画面が開いても登録内容がない、処理の一部が動かない |
| DB外の実ファイル | 画像・非公開添付・生成済み成果物、外部ストレージのオブジェクト | DBにファイル名はあるのに開けない |
| コードと依存関係 | Gitのコミット、composer.lock、package-lock.json、ビルド成果物または生成手順 | 古いDBに新しいコードを組み合わせ、列や処理が合わない |
| 秘密設定 | .env相当の設定、アプリの暗号鍵、外部接続先と資格情報の取得先 | 接続できない、暗号化済みデータを復号できない |
| サーバー構成 | Nginx / Apache、PHP-FPM、systemd、cron、通信制御、証明書の再取得方法 | Webだけ動いて定期処理が動かない、権限不足で書き込めない |
たとえばLaravelの添付なら、保存先の設定から実体をたどります。storage/app/publicだけを保存しても、非公開添付や外部ストレージに保存した画像は含まれません。キャッシュのように再生成できるファイルと、利用者の投稿のように再生成できないファイルを分けると、優先順位がつきます。
Gitでコードを管理していても、秘密設定は通常そこに入りません。秘密情報を公開リポジトリへ追加するのではなく、暗号化した保管先と取り出す権限を用意します。復号鍵が壊れたVPSの中にしかない構成では、バックアップ自体を開けなくなります。
取得頻度は「何時間分なら失ってよいか」から決める
RPOは許容するデータ損失の時間、RTOは復旧までに許容する時間の目標です。たとえば毎日02:00のデータだけを残して18:00に障害が起きれば、最大16時間分が戻らない計算です。取得に失敗していれば、さらに古い世代まで戻ります。
更新の少ない個人サイトと、注文を受け続けるサイトでは、同じ日次バックアップでも意味が違います。後者で数時間分の注文を失えないなら、取得間隔の短縮やDBログを用いた時点復旧を検討します。ただしDBだけ細かく戻せても、注文に紐づく添付が同じ時点へ戻せるとは限りません。
頻度を増やす
戻れる時点を新しくするための対策です。最後に取得・外部転送まで成功した時点を監視します。
世代を残す
誤削除に遅れて気づいたときの対策です。最新コピーだけでは、削除後の状態で上書きされます。
復元時間を測る
再開までの時間を確かめる対策です。取得頻度を上げても、転送やDB復元が速くなるわけではありません。
世代の決め方はバックアップの世代管理と復旧で詳しく扱っています。ここでは、選んだ世代の中身をそろえることに進みます。
DBと添付は、同じ時点の組にする
10:00に画像をコピーし、10:05に新しい画像が投稿され、10:10にDBを取得したとします。この組を復元すると、新しい画像の参照先はDBにあるのに、画像そのものがありません。
書き込みを止められる小規模アプリなら、メンテナンス時間にWebからの更新とキュー・定期ジョブの書き込みを止め、その間にDBと添付を取得する方法が分かりやすいです。止められない場合は、DBのログ、ファイルの版管理、アプリの保存順序などを含めた設計が必要になります。
MySQLの--single-transactionはInnoDBの一貫したダンプを得るための指定です。これはDB外の添付まで同じ時点にする機能ではありません。他のストレージエンジンや、取得中のALTER TABLEなどの定義変更も別に考える必要があります。MySQL 8.4公式資料で、この条件を確認できます。
実行例:MySQLと添付を別の復旧先へ戻す
2026年9月21日に、Dockerの隔離環境でMySQL 8.4.8を使って試しました。ネットワークと公開ポートを持たないコンテナ内に、取得元のbackup_labと空の復旧先restore_labを用意しています。添付は実ファイルです。OS全体や本番アプリの復旧ではなく、DBとファイルが揃って戻るかを確かめる実験です。
以下はLinuxのBashで実行する例です。検証DBだけを使い、復元先は空にしてください。ダンプにはテーブルを再作成するSQLが含まれるため、既存の本番DBへ流すとデータを上書きする危険があります。
1. 取得元のデータとファイルを用意する
MySQLで次を実行します。ここで使うデータはすべて架空です。
CREATE DATABASE backup_lab;
CREATE DATABASE restore_lab;
USE backup_lab;
CREATE TABLE attachments (
id INT PRIMARY KEY,
path VARCHAR(100) NOT NULL
) ENGINE=InnoDB;
INSERT INTO attachments VALUES (1, 'a.txt'), (2, 'b.txt');
続いて、新しい作業ディレクトリ内でファイルを作ります。
mkdir -p source/uploads bundle restore
printf 'first attachment\n' > source/uploads/a.txt
printf 'second attachment\n' > source/uploads/b.txt
DBのpathはファイル名を記録しているだけです。INSERTを実行しても添付そのものがDB内へ入るわけではない、という関係をこの例で確認できます。
2. DB・添付・照合用の記録をまとめる
実験では更新処理がなく、取得中にデータが変わらない状態にしています。
mysqldump -uroot --single-transaction --no-tablespaces \
--set-gtid-purged=OFF backup_lab > bundle/db.sql
tar -czf bundle/uploads.tar.gz -C source uploads
(cd source && sha256sum uploads/a.txt uploads/b.txt) > bundle/files.sha256
db.sqlはテーブルと行、uploads.tar.gzは実ファイル、files.sha256はファイル内容を照合する記録です。実運用では同じ世代のディレクトリに、取得開始・終了時刻、コードのコミット、DBの版、設定の保管先も記録します。
この例のroot・パスワードなし接続はネットワークのない使い捨て環境だけの設定です。本番の取得処理は必要な権限を持つ専用アカウントで行い、パスワードをコマンド引数へ直接書かないようにします。また、例は単純なテーブルだけです。ルーチンやイベント、DBユーザーの権限まで含む本番環境では、その取得と再作成を別途組み込みます。GTIDの指定もレプリケーション構成にそのまま流用しません。
3. 空のDBと別ディレクトリへ復元する
mysql -uroot restore_lab < bundle/db.sql
tar -xzf bundle/uploads.tar.gz -C restore
mysql -uroot -B restore_lab \
-e 'SELECT * FROM attachments ORDER BY id'
復元したDBから次の行を取得できました。
id path
1 a.txt
2 b.txt
ここではダンプ時に--databasesを付けず、インポート先のDBを指定しています。別の方法で作ったダンプにはCREATE DATABASEやUSEが含まれることがあるため、投入前に復元先を確認します。MySQL公式の復元手順も、この違いを説明しています。
4. DBが参照するファイルを確認する
行数だけでは添付の欠落を検出できません。復元したDBから参照先を取り出し、実ファイルと照合します。
mysql -uroot -N -B restore_lab \
-e 'SELECT path FROM attachments ORDER BY id' > bundle/references.txt
while IFS= read -r file; do
test -f "restore/uploads/$file" || printf 'MISSING %s\n' "$file"
done < bundle/references.txt
(cd restore && sha256sum -c ../bundle/files.sha256)
これは自分で作った単純なファイル名を照合する例です。実アプリでは保存先・相対パス・版の識別子を実装に合わせます。信頼できないパスをそのまま使う汎用スクリプトではありません。
実験では、正常復元、添付を一つ退避した場合、同じ名前のまま内容を変えた場合を比較しました。
| 試した状態 | DB参照先の存在確認 | ハッシュ照合 |
|---|---|---|
| DBと添付をすべて復元 | 不足なし | a.txt、b.txtともOK |
| b.txtを復旧先から退避 | MISSING b.txt | 存在確認の段階で不足が分かる |
| b.txtの内容だけ変更 | 不足なし | a.txtはOK、b.txtはFAILED |
最後にアーカイブから戻し直すと、両方がOKになりました。ここから分かるのは、DBの復元、参照先の存在、内容の一致は別々に確認する必要があるということです。ハッシュが一致しても、取得前から壊れていたファイルや、アプリの権限・暗号鍵の問題までは分かりません。最終確認では代表的な添付を実際に開きます。
保存先を分け、取り出せることを確かめる
上の実験は手順の確認なので同じコンテナ内に保存しましたが、本番バックアップを同じVPS内だけに置くと、ディスク障害で一緒に失います。外部ストレージへ転送し、転送完了と世代保持まで確認します。
保存先が別でも、侵害されたVPSの資格情報で全世代を削除できれば、被害が広がります。取得・転送用と過去世代の削除・管理用の権限を分け、オフライン保管や保持期間中の変更・削除を制限する機能も検討します。暗号化と復号鍵の保管はセットです。
スナップショットはサーバー全体を戻す補助手段になります。ただし、取得時のアプリ整合性や別環境への復元条件は事業者ごとに異なります。Amazon EBSではディスクへ書かれたデータが対象で、アプリやOSのメモリ上のキャッシュは含まれません。使うVPSの仕様を確認し、スナップショットと個別データのバックアップを役割分担させます。
本番の復旧演習では、アプリの操作まで通す
1. 復旧する世代を選ぶ
DB・添付・コード・設定の対応と取得成功時点を確認。誤削除への対処なら、削除前の世代を選びます。
2. 外部へ作用しない復旧先を作る
本番DBへの接続、メール送信、決済、外部API、キュー、定期ジョブを止めてから起動します。復元した設定の接続先を先に確認します。
3. 対応する版で戻す
実行環境、コード、秘密設定、DB、添付を復元。古いDBへ最新コードを置き、自動でマイグレーションする流れにはしません。
4. 利用者の操作と所要時間を記録
ログイン、代表データの表示、添付、権限、必要な復号処理を確認。復旧先の準備から再開可能になるまでの時間を測り、RTOと比べます。
復元できなかったときは「失敗」で終わらせず、失敗した対象と原因を残します。たとえば添付不足なら取得対象・取得時点・転送漏れ、復号できないなら鍵の世代、ジョブが動かないならサービス定義と実行権限を調べます。復元手順や保存先を変えた後にも再確認すると、手順書と実態のずれを見つけられます。
よくある質問
バックアップの成功通知だけでは足りませんか?
通知がDB取得だけの成功なのか、ファイル取得と外部転送まで含むのかで意味が違います。各段階の成否と、最後に復旧できるデータの時点を確認してください。復旧可能性は、別途戻して確かめます。
添付の数が多くても全件ハッシュを取りますか?
全件照合には読み出し時間と費用がかかるため、保存方式や必要な保証に応じて設計します。取得時のマニフェスト、ストレージの版管理、整合性検証機能なども選択肢です。代表ファイルを開くだけでは全件の健全性を証明できない点は区別します。
バックアップとレプリケーションは同じですか?
違います。レプリケーション先へ誤削除が反映されることもあるため、過去の状態へ戻るには世代やログの保管が必要です。可用性のための複製と、過去へ戻すための保存を分けて設計します。
まとめ
まず、自分のアプリのDB・添付・秘密設定・コード・サーバー設定を表に書き出します。それらを同じ復旧単位で保存し、外部へ転送した世代を隔離環境で戻してみる。この一連の作業を通すと、「保存はしていたが復旧に足りないもの」が具体的に見えてきます。
参考情報
公式資料は2026年9月21日に確認しました。