A successful backup command does not prove an application can be recovered. Start with one file, an explicit repository path, and a restore directory you can inspect on the host. Then add application consistency and off-site recovery.

Verification status

Area Evidence and limits
Technical review Official Restic references below reviewed on October 31, 2026.
Static checks Bash syntax, embedded Compose YAML, and path mappings checked.
Runtime Recovery lab run 41245705581 passed on October 31, 2026: initialization, backup, restore, retention preview, separately encrypted copy, wrong-password rejection, full copied-repository data check, and recovery without original materials.
Tested versions GitHub-hosted Ubuntu 24.04 amd64; Docker 28.0.4; Compose 2.38.2; Restic 0.19.1. Image digest: sha256:136600b6ff6843d61d355f7f71f460a166429f35de6fd11b568fece3c9a4d510.
Scope Disposable local file lab. Production databases, off-site recovery and scheduling require separate validation.

1. Separate source consistency from encryption

Restic encrypts repository data and deduplicates snapshots. It does not make a live database’s changing files application-consistent.

Source Preparation before backup
Ordinary files Ensure the selected files represent the state you intend to recover.
PostgreSQL or MariaDB Use a supported database dump or physical backup procedure.
Application with database and attachments Coordinate both parts according to its recovery documentation.
Stopped container data Confirm all writers are stopped and preserve required metadata and configuration.

A read-only mount prevents that container from writing to the source; it does not freeze other writers. Dump files must actually be included in the selected backup path. Any failed dump must fail the job instead of being logged as a warning followed by a success notification.

2. Create a disposable Compose project

Use Bash, Docker Engine, the Compose plugin, and OpenSSL. Select a reviewed official Restic image tag or digest compatible with your repository. The service uses the image’s Restic entrypoint; commands after the service name are Restic arguments.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
restic_lab_dir=$(mktemp -d -t restic-file-lab.XXXXXX)
cd "$restic_lab_dir" || exit 1
umask 077
mkdir source repository restored
printf 'restic-restore-check\n' > source/check.txt
openssl rand -base64 32 > restic-password

read -r -p 'Reviewed restic/restic image tag or digest: ' RESTIC_IMAGE
export RESTIC_IMAGE

cat > compose.yaml <<'EOF'
services:
  restic:
    image: ${RESTIC_IMAGE:?Set a reviewed restic/restic image}
    environment:
      RESTIC_REPOSITORY: /repository
      RESTIC_PASSWORD_FILE: /run/secrets/restic-password
    volumes:
      - ./source:/source:ro
      - ./repository:/repository
      - ./restored:/restore
    secrets:
      - restic-password

secrets:
  restic-password:
    file: ./restic-password
EOF

docker compose config --quiet
docker compose run --rm --interactive=false restic version

The commands use --interactive=false so a container cannot consume input intended for a later host-side prompt when these steps run from a script. Repository passwords come from mounted files.

This is a one-shot service, not a sleeping container with a background cron daemon. It needs no Docker socket. The password file is created in a new private directory; do not regenerate the password when reconnecting to an existing repository.

Keep production repository credentials and recovery instructions somewhere accessible if the Docker host is lost. See repository preparation.

3. Initialize once and back up

Run initialization only for this new empty repository. For an existing repository, use its saved password and inspect snapshots instead.

1
2
3
4
docker compose run --rm --interactive=false restic init
docker compose run --rm --interactive=false restic backup /source --host restic-file-lab --tag demo
docker compose run --rm --interactive=false restic snapshots
docker compose run --rm --interactive=false restic check

Check each command’s exit status before continuing. A nonzero backup result requires investigation even if some snapshot data was saved.

The fixed host label avoids grouping every new one-shot container under a different generated hostname. Record the snapshot ID returned by backup. See backing up.

4. Restore to a host-visible destination

Select the exact demo snapshot ID from the listing:

1
2
3
4
read -r -p 'Demo snapshot ID to restore: ' snapshot_id
test -n "$snapshot_id" &&
  docker compose run --rm --interactive=false restic restore "$snapshot_id" --target /restore
cmp source/check.txt restored/source/check.txt

Expected result to verify: the restored marker matches the original and cmp exits zero. The snapshot’s absolute source path is recreated under the target, so container path /restore/source/check.txt corresponds to host path ./restored/source/check.txt.

Restoring into a container’s unmounted /tmp directory would not put files in the host’s /tmp directory. Keep mappings explicit. Never test by copying a restore directly over a live database volume.

Use Restic restore guidance for selecting files. The restore –path filter selects snapshots; it does not mean “restore only this directory.” Use documented include/exclude or subtree options when needed.

5. Review retention before deleting anything

For the demo group, preview a policy:

1
2
3
docker compose run --rm --interactive=false restic forget --dry-run \
  --host restic-file-lab --tag demo \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 3

Inspect the keep/remove lists and grouping behavior before applying a policy. Forget removes snapshot references; prune reclaims eligible repository data. Do not attach automatic pruning to your first backup exercise.

Plan maintenance windows and verify the repository afterwards. A partial data check does not read every stored pack, and a repository check is not an application restore test. See retention.

6. Add an independent off-site repository

A repository beside the source on the same host is useful for this exercise, but cannot survive loss of that host.

Choose a supported remote backend, configure its credentials, and initialize a separate repository once. Use Restic’s supported copy workflow to transfer selected snapshots between repositories, or run a separate backup against the same prepared source. One backup invocation targets one repository.

With copy, explicitly configure destination access and source repository/password options. Do not assume local and remote passwords are interchangeable. Verify remote snapshots, check the remote repository, and restore from it using only the off-site credentials and recovery materials.

See working with repositories. Retention is independent for each repository. Do not substitute an uncoordinated file mirror of a changing repository for a tested recovery workflow.

Rehearse recovery from a separately encrypted copy

This optional extension uses a second local repository to test copy and credential separation. It does not test a remote backend or survive loss of this host. Continue in the disposable lab from above.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mkdir repository-copy recovered-copy
openssl rand -base64 32 > copy-password
cat > compose.copy.yaml <<'EOF'
services:
  restic:
    volumes:
      - ./repository-copy:/repository-copy
    secrets:
      - copy-password
  recovery:
    image: ${RESTIC_IMAGE:?Set a reviewed restic/restic image}
    environment:
      RESTIC_REPOSITORY: /repository
      RESTIC_PASSWORD_FILE: /run/secrets/copy-password
    volumes:
      - ./repository-copy:/repository
      - ./recovered-copy:/restore
    secrets:
      - copy-password
secrets:
  copy-password:
    file: ./copy-password
EOF

docker compose -f compose.yaml -f compose.copy.yaml run --rm --interactive=false restic \
  --repo /repository-copy --password-file /run/secrets/copy-password init
docker compose -f compose.yaml -f compose.copy.yaml run --rm --interactive=false restic \
  --repo /repository-copy --password-file /run/secrets/copy-password \
  copy --from-repo /repository --from-password-file /run/secrets/restic-password

The destination has its own password. The recovery service mounts only that repository, its password, and a fresh restore directory. It has no access to the original source, repository, or password. List and verify the copied snapshot, then select its ID:

1
2
3
4
5
6
7
docker compose -f compose.yaml -f compose.copy.yaml run --rm --interactive=false recovery snapshots
docker compose -f compose.yaml -f compose.copy.yaml run --rm --interactive=false recovery check --read-data
read -r -p 'Copied snapshot ID to restore: ' copied_snapshot_id
test -n "$copied_snapshot_id" &&
  docker compose -f compose.yaml -f compose.copy.yaml run --rm --interactive=false recovery \
    restore "$copied_snapshot_id" --target /restore
printf 'restic-restore-check\n' | cmp - recovered-copy/source/check.txt

For this small lab, check --read-data reads all stored backup data. Budget bandwidth and time before using it on a large remote repository. Matching the marker verifies this file exercise; it does not validate a database or an application’s recovery.

7. Automate only the verified workflow

Move the validated project to a stable operational path and adapt the source mounts to prepared application data. Before scheduling, require:

  • Database dumps written to a mounted staging directory, with errors propagated and partial files excluded.
  • A mount check for external backup storage so a missing mount cannot silently redirect writes to the root filesystem.
  • A stable host label, explicit source selection, overlap prevention, and retained logs.
  • Success notification only after all required backup stages succeed, plus alerts for failed or missing runs.
  • Separate retention and integrity checks, and recurring isolated application restore tests.

Test the scheduled command under its actual user and environment. Do not source an arbitrary Compose .env file as shell code or assume a container image includes Bash, Docker CLI, curl, or a scheduler.

Keep the demo files until you have reviewed the restore result. No services remain running after these one-shot commands. Any repository deletion must be a separate, deliberate cleanup decision.

Choose the next recovery exercise

This guide demonstrates files and separately encrypted repository copies. For a complete PostgreSQL dump → Restic copy → fresh database import, use the integrated database recovery lab. Its runtime evidence covers local CI repositories, not your actual remote backend. For guest recovery, follow PBS setup; for copy planning, start with the backup strategy.

Continue reading