SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

Docker Composeのバックアップと安全なアップグレード方法

Docker Composeのバックアップで保存すべきComposeファイル、.env、volume、DBダンプを整理します。restoreの検証方法と、pull前に行う安全なアップグレード手順も解説します。

Docker Compose スタックのバックアップに含めるもの

Docker Compose スタックのバックアップには、4 つの独立した要素を含める必要があります。どれか 1 つでも失うと、アプリケーションは復旧できません。対象は、Compose ファイル、その横にある .env、すべてのボリュームの内容、そしてデータベース自身のクライアントで作成したデータベースダンプです。コンテナの実行中にデータベースのファイルをコピーしても、バックアップにはなりません。アップグレードでも同じ一覧を使用します。ただし、1 つルールがあります。pull の前にバックアップを取得してください。スキーマのマイグレーションは前進方向に実行されるよう作られており、多くのプロジェクトには元に戻す手段がないためです。

以下では、スタックがすでにデプロイされ、docker compose ps で稼働中であることを前提とします。例では、/srv/myapp にあるプロジェクトディレクトリと、app および db という名前のサービスを使用します。実際の環境の名前に置き換えてください。コマンドは意図的に汎用的にしています。重要な部分であるボリュームとデータベースは、アプリケーションが何であっても同じ方法で扱えるためです。

実際のスタックが保存しているデータを確認する

cd /srv/myapp
docker compose ps
docker compose config --volumes
docker volume ls --filter label=com.docker.compose.project=myapp

docker compose config --volumes は、compose ファイルで宣言されている named volume の短い名前を出力します。docker volume ls は、それらの volume がディスク上で実際に使用している名前を出力します。2 つの一覧が異なるのは、Compose が先頭にプロジェクト名を付けるためです。ファイルで db_data と記述した volume は、実際には myapp_db_data として存在します。プロジェクト名の既定値はディレクトリ名です。そのため、ディレクトリ名を変更すると、スタックは空の volume の新しいセットを参照し、以前の volume はデータを保持したまま残ります。以下の各コマンドでは、docker volume ls から実際の名前を指定する必要があります。

bind mount は、どちらの一覧にも表示されません。compose ファイルでは、コロンの左側にホスト側のパスがあるエントリが該当します。./config:/app/config。これらはホスト上の通常のディレクトリなので、通常のツールでアクセスできます。named volume は /var/lib/docker/volumes/ 配下に保存され、docker volume inspect --format '{{.Mountpoint}}' myapp_db_data で 1 つの volume の正確なパスを出力できます。スタックがどちらの方式を使用しているかによってコピー方法は変わります。bind mount と named volume の比較 で、その違いを詳しく説明しています。

次に、確認したものを 2 つのグループに分類します。一方は、再作成できない状態を保持する volume です。アップロードしたファイル、生成された鍵、データベース本体、アプリにユーザーが入力した内容などが該当します。もう一方は、サムネイルや検索インデックスなど、アプリが自動的に再構築できる派生データを保持する volume です。後者をバックアップしても、ディスク容量と復元時間が増えるだけで、メリットはありません。Redis のキャッシュ volume が最も分かりやすい例です。これを失っても、最初のリクエストが遅くなるだけです。

compose ファイルと .env ファイルをバックアップする

両方のファイルはホスト上で隣り合って配置されており、どちらも volume 内にはありません。.env にはデータベースのパスワード、アプリケーションシークレット、API トークンが保存されます。そのため、複数の volume を動作するアプリケーションに戻すために重要なファイルです。通常は .gitignore にも記載されるため、「設定は git に保存している」というバックアップ方針では、最も重要な単一ファイルが対象外になります。env ファイルにシークレットを保存する方法が適切です。その場合、バックアップにも対応する責務が生じます。

sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/backups/myapp
cp -a compose.yaml .env /srv/backups/myapp/
chmod 600 /srv/backups/myapp/.env

スタックが使用するすべての compose ファイルをコピーしてください。最初のファイルだけでは不十分です。-f compose.yaml -f compose.prod.yaml で起動したスタックを同じ構成で復元するには、両方のファイルが必要です。また、複数の compose ファイルのマージ方法によって、実際にコンテナへ渡される値が決まります。

.env と volume には関連する注意点があります。公式の Postgres イメージは、空のデータディレクトリを初期化するときにだけ POSTGRES_PASSWORD を読み取ります。後からこの値を変更しても、データベース内のパスワードは変更されません。先月の volume を今日の .env と一緒に復元すると、両方のファイルを確認した限りでは正しく見えても、アプリケーションは FATAL: password authentication failed for user "appuser" で接続に失敗します。.env と volume は、同じ時点のものを同じバックアップにまとめて保管してください。

データベース固有のクライアントでダンプする

データベースサーバーは、常にファイルへ書き込みます。サーバーの稼働中に取得した tar/var/lib/postgresql/data は、書き込み前のページと書き込み後のページを混在させるため、アーカイブには復元できない可能性がある複数の時点の状態が含まれます。ダンプツールは単一のトランザクション内を読み取るため、ファイルには一貫した1時点の状態が保存されます。この違いが、バックアップと単なるコピーを分けます。

docker compose exec -T db sh -c \
  'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
  > /srv/backups/myapp/db-$(date +%F).dump

-T は付けたままにします。これは TTY の割り当てを無効にします。TTY を接続すると、Docker がシェルへ出力を渡す途中で出力ストリームを変換するため、バイナリダンプが破損します。復元に失敗するまで、この問題には気付きません。単一引用符も重要です。これによりホストのシェルが $POSTGRES_USER を展開せず、コンテナ内のシェルが、compose ファイルで設定済みの値を使って展開します。-Fc は custom format で書き込みます。この形式は処理中に圧縮され、後から pg_restore でオブジェクトを取り出せます。

ロールとそのパスワードは特定のデータベースの外部に保存されるため、これらも取得します。

docker compose exec -T db sh -c 'pg_dumpall -U "$POSTGRES_USER" --globals-only' \
  > /srv/backups/myapp/globals.sql

次に、そのファイルがエラーメッセージではなくダンプであることを確認します。

ls -lh /srv/backups/myapp/
head -c 5 /srv/backups/myapp/db-$(date +%F).dump

custom format のダンプは、5バイトの PGDMP で始まります。0バイトのファイル、または pg_dump: で始まるファイルは、コマンドが失敗したことを示します。シェルはコマンドの実行前に出力ファイルを作成するため、ダンプに失敗しても、もっともらしい名前とタイムスタンプのファイルが残ります。これは、最も一般的なサイレントバックアップ失敗です。

MariaDB または MySQL では、クライアントは変わりますが、手順の形は変わりません。

docker compose exec -T db sh -c \
  'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction --databases "$MARIADB_DATABASE"' \
  > /srv/backups/myapp/db-$(date +%F).sql

--single-transaction は、書き込みをブロックせずに InnoDB テーブルの一貫したダンプを作成します。MySQL イメージではコマンドが mysqldump で、変数は MYSQL_ROOT_PASSWORDMYSQL_DATABASE です。現在の MariaDB イメージでは、mysqldumpmariadb-dump の互換名として引き続き使用できます。コマンドラインで指定したパスワードは、ダンプの実行中、コンテナのプロセス一覧に表示されることに注意してください。

SQLite には、固有の注意点があります。データベースは1つのファイルですが、最近のトランザクションが、その隣にある別の -wal ファイルに残っている場合があります。そのため、.db だけをコピーすると、最新の書き込みが欠落したデータベースになります。イメージにクライアントが含まれている場合は、アプリケーションの稼働中に sqlite3 /data/app.db ".backup '/data/app-backup.db'" で一貫したコピーを作成できます。含まれていない場合は、コンテナを停止し、.db ファイルを -wal-shm の関連ファイルと一緒にコピーします。

データベースが stack 内ではなくホスト上で稼働している場合は、docker compose exec プレフィックスなしで同じコマンドを使用できます。次回の再構築前に、Docker またはホスト上でデータベースを実行する も確認してください。

ボリュームを取得する

名前付きボリュームには、手作業で編集すべきホスト側のパスがありません。そのため、一時的なコンテナにマウントし、そこからアーカイブします。

docker run --rm \
  -v myapp_uploads:/data:ro \
  -v /srv/backups/myapp:/backup \
  alpine:3 tar czf /backup/uploads.tar.gz -C /data .

ヘルパーコンテナは、ボリュームを /data に読み取り専用で、バックアップディレクトリを /backup にマウントし、アーカイブをホスト側へ書き出します。--rm により、tar が終了するとすぐにヘルパーが削除されます。:ro は重要です。tar コマンドを入力し間違えても、ソースを破損させられないためです。-C /data . によって、復元先が正しくなります。ボリュームのルートからの相対パスとして、すべてのパスを保存するためです。代わりに tar czf /backup/uploads.tar.gz /data とすると、すべてのパスの先頭に data/ が付きます。その結果、復元時にボリューム内へ /data/data が作成され、アプリからは空のディレクトリに見えます。コンテナ内で tar を root として実行したため、アーカイブの所有者は root です。問題になる場合は sudo chown "$USER" /srv/backups/myapp/uploads.tar.gz を実行してください。復元したファイルをアプリが読み取れない場合は、PUID と PGID がファイル所有者を決める仕組みも確認してください。

名前付きボリュームごとに 1 回実行します。バインドマウントにはコンテナは不要です。tar czf /srv/backups/myapp/config.tar.gz -C /srv/myapp/config . でホスト上から同じ処理を実行できます。

アプリを停止する必要があるかどうかを、ボリュームごとに判断します。アプリがボリューム内のファイルを書き換えている最中に tar を実行すると、書き込み途中のファイルを取得する可能性があります。アップロードディレクトリのように、ファイルを 1 回書き込んだ後は読み取りだけを行う場合、このリスクは小さいです。それ以外では、コピー中だけ docker compose stop app で対象サービスを停止し、その後 docker compose start app を実行してください。stop はコンテナとボリュームをそのまま残します。この用途に適した動作です。どちらかを入力する前に、down と stop の違いを確認しておく価値があります。

データベースボリュームの tar を、データベースのバックアップとして扱わないでください。バックアップはダンプです。停止したデータベースのボリュームアーカイブは、迅速な再構築に使える手段にすぎません。

作業手順

  1. compose ファイルと .env をバックアップディレクトリにコピーします。
  2. データベースが稼働している間にダンプを取得します。
  3. ボリュームの内容をその場で変更する場合は、アプリコンテナを停止します。
  4. 各 named volume と各 bind mount ディレクトリをアーカイブします。
  5. 停止したものを再起動し、docker compose ps で確認します。
  6. スタックが使用しているイメージタグとダイジェストを記録します。
  7. バックアップディレクトリ全体をこのサーバーの外部へコピーします。

手順 7 は、後回しにされがちです。

サーバー外部にコピーを保管する

スタックと同じディスク上にあるバックアップは、自分の操作ミスからは保護できますが、それ以外の障害からは保護できません。ボリュームの障害、サーバーの削除、アカウントの紛失が1つ発生するだけで、2つのコピーを同時に失います。ディレクトリは、この VPS ではないストレージへ、スケジュールに従ってコピーし、保持ポリシーを設定してください。VPS からの restic バックアップでは、リポジトリの設定、保持用フラグ、チェックコマンドを説明しているため、ここでは繰り返しません。

restic はパイプからダンプを直接読み取ることもできます。これにより、平文のデータベースをディスクに保存せずに済みます。

docker compose exec -T db sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc' \
  | restic backup --stdin --stdin-filename db.dump

使用するツールに関係なく、スケジュールは systemd timer または cron job に登録し、ジョブの失敗を確認できる場所へ報告させてください。出力先のないバックアップスクリプトは、誰にも発見されないまま6か月間停止し続ける可能性があります。

バックアップが機能することをリストア訓練で確認する

誰もリストアしていないバックアップは、仮説にすぎません。以下の訓練では、稼働中のスタックに影響を与えず、その横で動作する 2 つ目のスタックにリストアします。そのため、本番環境はサービスを提供し続け、入力した内容が本番環境に到達することもありません。

仕組みの鍵はプロジェクト名です。Compose はディレクトリ名からプロジェクト名を取得し、作成するすべてのコンテナとボリュームに付けます。バックアップを新しいディレクトリにコピーすると、リストアしたスタックには専用のボリュームが自動的に割り当てられます。

sudo install -d -m 700 -o "$USER" -g "$(id -gn)" /srv/myapp-restore
cd /srv/myapp-restore
cp /srv/backups/myapp/compose.yaml /srv/backups/myapp/.env .

コピーした compose ファイルを編集し、公開するホストポートが稼働中のスタックと競合しないようにします。8080:8080 の代わりに 18080:8080 を設定するか、コピー先の .env でポートを設定している変数を変更してください。その後、何も起動せずにコンテナと空のボリュームを作成します。

docker compose create
docker volume ls --filter label=com.docker.compose.project=myapp-restore

2 つ目のコマンドでは、本番環境と同じボリューム名の先頭に myapp-restore_ が付いていることを確認できます。ボリュームにデータを展開し、データベースだけを起動してダンプをロードします。

docker run --rm -v myapp-restore_uploads:/data -v /srv/backups/myapp:/backup \
  alpine:3 tar xzf /backup/uploads.tar.gz -C /data
docker compose up -d db
docker compose exec -T db sh -c \
  'pg_restore -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists' \
  < /srv/backups/myapp/db-2026-08-16.dump

--clean --if-exists は、各オブジェクトを再作成する前に削除します。これにより、リストアを繰り返し実行できます。このオプションがない場合、すでにテーブルが存在するデータベースに 2 回目のリストアを実行すると、pg_restore: error: could not execute query: ERROR: relation "users" already exists で停止します。

続いて残りのサービスを起動し、ユーザーと同じ方法で確認します。

docker compose up -d --wait
docker compose exec -T db sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt"'
docker compose logs --tail=50

docker compose up -d --wait は、すべてのサービスが running または healthy になるまで待機します。サービスがその状態にならない場合は、終了ステータスが 0 以外になります。このため、処理をスクリプト化できます。サービスが healthy にならない場合は、docker compose ps で状態を確認します。Compose のヘルスチェックでは、その列で確認している内容を説明しています。その後、代替ポートでアプリを開き、実際のアカウントでログインします。1 件のレコードを作成し、ボリュームに保存されているファイルを 1 つ開きます。この 2 つが証拠です。ダンプ、ボリューム、そして両者の整合性がリストアされています。ログインページが表示されることだけを確認する訓練では、データについて何も証明できません。

訓練が成功したら、環境を削除します。

docker compose down -v

-v が正しいフラグになるのは、この場所だけです。本番ディレクトリで同じコマンドを実行すると、保護しようとしているボリュームが削除されます。

Compose スタックをアップグレードする方法

現在使用しているバージョンと、移行先のバージョンの間にあるすべてのバージョンについてリリースノートを読み、breaking と migration という語を検索します。複数のメジャーバージョンをまたぐアップグレードに対応していないプロジェクトは、その点をリリースノートに記載しています。また、移行処理が実行を拒否する場合、その時点ではスキーマの一部がすでに変更されていることがあります。

変更する前に、現在の構成を記録します。

docker compose images
docker image inspect --format '{{index .RepoDigests 0}}' postgres:16.4

docker compose images には、各サービスが現在使用しているイメージとタグが一覧表示されます。イメージを正確に識別できる値はダイジェストだけです。タグは、いつでも別のイメージを指すように変更できるためです。

上のセクションで作成したバックアップを取得し、サーバー外にもコピーします。パッチリリースの場合も同様に実施します。準備をやめたときに、アップグレードは安価に済まなくなります。

次に、Compose ファイルでバージョンを固定します。latest はバージョンではないためです。

services:
  db:
    image: postgres:16.4

image: postgres:latest を指定すると、docker compose pull はそのタグが現在指しているイメージを取得します。そのため、昨日使用していたイメージを特定できません。タグを固定すれば、アップグレードは1行の編集になり、git diff で内容を確認できます。さらに1行編集するだけで元に戻せます。アプリケーションイメージも同じ方法で固定し、プロジェクトのリリースページから正確なバージョンを取得します。

イメージを取得して、コンテナを再作成します。

docker compose pull
docker compose up -d --wait

docker compose up -d はファイルと実行中のコンテナを比較し、イメージまたは設定が変更されたサービスだけを再作成します。名前付きボリュームには変更を加えないため、新しいコンテナは既存のデータを使用して起動します。これがアップグレードの目的です。同時に、新しいバージョンの初回起動時にスキーマ移行が実行されることが多いため、リスクでもあります。

実行状況を監視します。

docker compose ps
docker compose logs -f --tail=100 app

失敗したコンテナには、docker compose psSTATUS 列に Exited (1) と表示されます。失敗の理由は、そのコンテナのログの末尾にあります。移行エラーはここでは明確に表示されますが、それ以外の場所では確認できません。ログが落ち着いたら、ログインしてアプリを1分ほど操作します。

docker compose pullno space left on device で停止する場合、通常の原因は古いイメージレイヤーです。未使用の Docker イメージを削除すると、空き容量を回復できます。削除はアップグレードが正常に動作することを確認した後に実行し、先に削除しないでください。古いレイヤーは迅速なロールバックに使用するためです。

アップグレードに失敗した場合のロールバック方法

ケースは 2 つあり、必要な作業量は大きく異なります。新しいバージョンでスキーマが変更されていない場合、ロールバックは 1 行で済みます。compose file の古い tag に戻して、docker compose up -dを実行します。コンテナは置き換えられますが、volume はそのまま残り、古いコードが自分で書き込んだデータを読み取れます。

新しいバージョンでスキーマが移行された場合、古いコードはそのスキーマを読み取れなくなります。Migration は前方向への実行を前提に書かれており、多くのプロジェクトでは downgrade script が提供されていません。そのため、古いバージョンは起動しても、名前を変更または削除された column に対する最初の query で失敗します。エラーは ERROR: column "avatar_url" does not existのような形式になります。復旧には、pull 前に取得した dump を使います。古い tag に戻し、database volume を取り外して空の状態で再作成し、dump を restore してから起動します。その dump がなければ、復旧する方法はまったくありません。だからこそ、pull より先に backup を取得します。

Postgres の major version は、この問題が最も顕著です。しかも、失敗するのは rollback 時ではなく upgrade 時なので、予想しにくい場合があります。ディスク上の format は major release ごとに変わります。postgres:16.4postgres:17.2に変更して docker compose up -dを実行すると、新しい server は起動を拒否します。

FATAL:  database files are incompatible with server
DETAIL:  The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.2.

image は pg_upgradeを自動的には実行しません。Compose stack 内でサポートされる手順は、dump、replace、restore です。古い version を起動したまま dump を取得し、docker compose downを実行して database volume を削除し、新しい tag を設定し、docker compose createで空の data directory を作成してから、database を起動し、dump を restore し、残りを起動します。新しい major が実際の traffic を 1 日処理するまで、古い dump は保持してください。同じ major 内の minor upgrade、たとえば 16.4 から 16.9 への upgrade では、この作業は必要ありません。format が同じ major 内で安定しており、container はそのまま起動するためです。

VPS のスナップショットはバックアップですか?

スナップショットはバックアップの代わりではなく、補完するものです。両者は異なる方法で失敗します。スナップショットはハイパーバイザー上でディスク全体をコピーするため、バックアップし忘れた部分も含め、マシン全体を数分で元に戻せます。そのため、特定の用途に適しています。アップグレードでサーバーが壊れ、20 分前の状態に戻したい場合です。

それ以外の用途には適していません。復元単位がマシン全体なので、削除したテーブル 1 つを復旧するには、サーバー全体をどこかに復元して、その中からテーブルを取り出す必要があります。保持期間も通常は短いです。コピーは通常、サーバーと同じプロバイダーアカウントに保存されるため、アカウントを失うとサーバーとスナップショットを同時に失います。また、稼働中のマシンのスナップショットでは、データベースが書き込み途中の状態になる可能性があります。そのため、データベースは初回起動時にクラッシュリカバリを実行し、処理中だったトランザクションは失われます。

両方を使用してください。スナップショットは、アップグレード作業中に変更を元に戻すための手段です。ダンプは、アカウントを削除されても残るコピーです。スナップショットとバックアップの違いでは、それぞれが実際にどの障害をカバーするかを説明しています。同じバックアップディレクトリがあれば、スタックを新しい VPS に移行する作業も、記憶を頼りに再構築するのではなく、定型的な手順として実行できます。

発生する問題と確認できる内容

down の volumes フラグ。 docker compose down -v はファイルで宣言された名前付きボリュームを削除し、Compose は Volume myapp_db_data Removed と表示して確認します。取り消しはできません。通常の docker compose down では、ボリュームはそのまま残ります。長い形式の docker compose down --volumes を入力し、破壊的なフラグを明示的に指定するようにしてください。

マジック文字列がないダンプ。 pg_restore: error: did not find magic string in file header は、そのファイルがアーカイブではないことを示します。よくある原因は docker compose exec-T がないことです。TTY が接続されていると、ストリームがシェルに渡る途中で変換され、バイナリダンプが破損するためです。-T を指定してダンプを取り直し、head -c 5 で先頭 5 バイトを確認してください。

変更されないパスワード。 リストア後の FATAL: password authentication failed for user "appuser" は、.env とデータディレクトリが異なる時点のものであることを示します。イメージがこのパスワードを設定するのは、空のデータディレクトリを作成するときだけです。そのため、後から .env を編集しても、データベース内部は変わりません。一致する .env をリストアするか、ALTER USER でデータベース内のパスワードを変更してください。

2 つ目の空のボリューム。 Docker は必要に応じてボリュームを作成します。そのため、s がない状態で docker run -v myapp_upload:/data を実行すると、新しく空のボリュームに書き込み、成功したと報告します。docker volume ls を実行すると、2 つの名前が表示され、そのうち 1 つには何も入っていません。ボリューム名は記憶で入力せず、docker volume ls からコピーしてください。

本番環境を対象にしたリストア。 /srv/myapp-restore ではなく /srv/myapp でリストアコマンドを実行すると、バックアップで稼働中のデータを上書きします。どちらの環境でもコマンドは同じに見えます。各リストアコマンドの前に pwd を確認し、検証作業は専用のディレクトリで行ってください。

FAQ

docker compose down でデータは削除されますか?

いいえ。docker compose down はコンテナとデフォルトネットワークを削除しますが、名前付きボリュームと bind mount には影響しません。docker compose down -v はファイルで定義した名前付きボリュームを削除するため、この操作は元に戻せません。bind mount はホスト上のディレクトリなので、Compose が削除することはありません。バックアップ中にサービスだけを停止し、それ以外を変更したくない場合は、代わりに docker compose stop を使用してください。

pg_dump を実行せずに Postgres のデータディレクトリをコピーできますか?

コンテナを停止している場合に限り可能です。サーバーの実行中はファイルが変更されるため、コピーに異なる時点の内容が混在し、復元しても再現できない状態になることがあります。ファイル単位のコピーは、1 つの Postgres メジャーバージョンにも依存するため、別のバージョンでは起動できません。コンテナを停止し、ボリュームをアーカイブしてから再度起動してください。ただし、そのコピーは唯一のバックアップではなく、迅速な再構築手段として扱います。移植性のあるコピーであり、実際に復元に使用するのはダンプです。

Compose で Postgres を新しいメジャーバージョンにアップグレードするにはどうすればよいですか?

タグを変更するだけでは不十分です。新しいサーバーは古いデータディレクトリでの起動を拒否し、The data directory was initialized by PostgreSQL version 16, which is not compatible with this version 17.2 をログに出力します。古いバージョンを実行したまま pg_dump を実行し、続いて docker compose down を実行します。その後、データベースボリュームを削除して新しいタグを設定し、docker compose create を実行して空のボリュームを作成します。データベースを起動し、ダンプを復元してください。新しいバージョンが実際のネットワークトラフィックを処理するまで、古いダンプを保持します。

バックアップはどのくらいの頻度で実行し、どのくらいの期間保持すべきですか?

やり直してもよい作業量に合わせて間隔を決めます。個人または小規模チームの構成では、毎晩のバックアップが適しています。さらに、アップグレードの直前には手動バックアップを 1 回追加します。保持期間については、すぐには気付かなかった障害をカバーできるだけの履歴を残してください。金曜日に破損したテーブルが見つかった場合、木曜日の夜のコピーでは役に立たないためです。restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune は妥当な開始方針です。スケジュールにかかわらず、四半期に 1 回はそのバックアップから復元してください。これを実行するまでは、バックアップではなくファイルを保持しているだけです。

バックアップを取得するために、スタック全体を停止する必要がありますか?

通常は必要ありません。データベースダンプはサーバーの実行中でも整合性が保たれるため、データベースを停止する必要はありません。問題になるのはボリュームです。アプリが uploads ディレクトリのようなファイルを追加するだけなら、実行中のアーカイブでも十分安全です。ファイルをその場で書き換える場合は、コピーの間だけ docker compose stop app でそのサービスを停止し、完了後に再度起動してください。データベースを稼働させたままアプリだけを停止する方法が、通常は確保できる最短の安全な停止時間です。