A utility for backing up and restoring Docker volumes.
Runs as a Docker/Podman container. Provides two scripts: backup and restore.
- Features
- How It Works
- Quick Start
- Mounting Volumes
- backup script
- restore script
- Environment Variables
- Backup File Formats
- Examples: Compression and Encryption
- Examples: S3
- Examples: All Volumes
- Backing up every Docker volume on the host
- Dry Run
- Automation with cron
- Production Recommendations
- Troubleshooting
- FAQ
Backup:
- Back up a single volume to an archive
- Back up all volumes from
/volumes - Save archives locally or upload to S3
- Compress with
pigz/gzip - Encrypt with GPG (symmetric passphrase or asymmetric public key)
- Run backups for multiple volumes in parallel
- Assign a shared timestamp to a group of backups
- Write a SHA256 checksum alongside every archive (local and S3)
- Preserve sparse files, POSIX ACLs, and extended attributes when GNU
taris available
Restore:
- Restore a single volume from a local file or from S3
- Restore all volumes from a backup directory
- Restore only selected volumes
- Filter by a specific timestamp
- Clear a volume before restoring (
-C) - Run restores for multiple volumes in parallel
- Verify the archive against its SHA256 checksum automatically (local and S3) when one is present
- Staged extraction: the archive is unpacked and verified in a staging directory inside the target volume, and only swapped in after a successful and verified extract — a failure during extract/verify leaves the original data intact
Docker volume → tar archive → [gzip compression] → [GPG encryption] → local file or S3
The filename is generated automatically:
<VOLUME>_<YYYYMMDD_HHMMSS>.tar
<VOLUME>_<YYYYMMDD_HHMMSS>.tar.gz
<VOLUME>_<YYYYMMDD_HHMMSS>.tar.gpg
<VOLUME>_<YYYYMMDD_HHMMSS>.tar.gz.gpg
A .sha256 checksum file is written alongside each archive — for both local backups and uploads to S3.
local file or S3 → [SHA256 verification] → [GPG decryption] → [gzip decompression] → Docker volume
The format is detected automatically from the file extension.
If a .sha256 companion file is present, the archive is verified against it. A mismatch aborts the restore and leaves the target volume untouched.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -v postgres_dataThe archive will appear at ./backups/postgres_data_<timestamp>.tar.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -i postgres_data_20250411_120000.tar -t postgres_data -Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -T 20250411_120000 -CThe container reads and writes volumes from /volumes inside itself.
Mount each volume you want to work with using --volume <name>:/volumes/<name>.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -cIf you use file-based encryption (rather than environment variables):
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--volume ./gpg:/gpg \
ghcr.io/vansergen/vb backup -v postgres_data -c -e -k public.ascThe key file must be present at ./gpg/public.asc (or another path specified with -k).
| Mode | Command |
|---|---|
| Single volume | backup -v <name> [options] |
| All volumes | backup -a [options] |
| Selected volumes from all | backup -a -v <name> [-v <name> ...] [options] |
| Option | Description |
|---|---|
-v <name> |
Volume name (directory inside /volumes) |
-a |
Back up all volumes found in /volumes |
-d <dir> |
Backup directory override (default: /backups) |
-T <ts> |
Set timestamp manually (format: YYYYMMDD_HHMMSS) |
-j <N> |
Parallel jobs in -a mode (default: 1) |
-c |
Compress with pigz if available, otherwise gzip |
-e |
Asymmetric GPG encryption (public key) |
-s |
Symmetric GPG encryption (passphrase) |
-b <bucket> |
Upload to S3 bucket |
-f <path> |
Folder / prefix inside the S3 bucket |
-k <file> |
Public key file inside /gpg (default: public.gpg) |
-n |
Dry run: validate and print what would be done without creating any files |
-h |
Show help |
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -v postgres_datadocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -v postgres_data -cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume /mnt/nas/backups:/custom-backups \
ghcr.io/vansergen/vb backup -v postgres_data -c -d /custom-backupsdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -v postgres_data -v redis_data -cOnly postgres_data and redis_data are backed up, even though uploads is also mounted.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -c -T 20250411_120000Or via environment variable:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
--env BACKUP_TIMESTAMP=20250411_120000 \
ghcr.io/vansergen/vb backup -a -cA shared timestamp lets you restore the entire group in one command later:
docker run --rm ... ghcr.io/vansergen/vb restore -a -T 20250411_120000 -Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -c -j 3Runs up to 3 backups concurrently.
| Mode | Command |
|---|---|
| Single volume | restore -i <file|s3://...> -t <name> [options] |
| All volumes | restore -a [options] |
| Selected volumes from all | restore -a -v <name> [-v <name> ...] [options] |
Note: The
-amode only works with local files inBACKUP_ROOT. To restore from S3, use single mode with-i s3://....
| Option | Description |
|---|---|
-i <source> |
Source: filename in BACKUP_ROOT, a file path, or s3://bucket/key |
-t <name> |
Target volume name (directory in /volumes) |
-a |
Restore all volumes from files in BACKUP_ROOT |
-d <dir> |
Backup directory override (default: /backups) |
-T <ts> |
In -a mode: restore only backups with this exact timestamp |
-v <name> |
In -a mode: filter by volume name (repeatable) |
-j <N> |
Parallel jobs in -a mode (default: 1) |
-C |
Clear the target volume before restoring |
-s |
Symmetric GPG decryption (passphrase) |
-k <file> |
Private key file for asymmetric GPG decryption (default: private.gpg) |
-n |
Dry run: validate and print what would be done without restoring |
-h |
Show help |
Note on
-C: Without-C, restore will fail if the target volume is not empty. This protects against accidental data loss. With-C, the archive is fully extracted and verified in a staging directory inside the target volume before any existing data is touched, so a failure during extraction, decryption, decompression, or checksum verification (network drop, disk error, wrong passphrase, checksum mismatch) leaves the original data intact. After verification succeeds, the script clears the old contents and moves the new ones in — an interruption during this final step (SIGKILL, power loss, disk error) can leave the volume in a partially restored state.
# Filename only — looked up inside BACKUP_ROOT (/backups)
restore -i postgres_data_20250411_120000.tar.gz -t postgres_data
# Absolute path
restore -i /mnt/nas/postgres_data_20250411_120000.tar.gz -t postgres_data
# S3
restore -i s3://my-bucket/daily/postgres_data_20250411_120000.tar.gz -t postgres_datadocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -i postgres_data_20250411_120000.tar -t postgres_data -Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -i postgres_data_20250411_120000.tar.gz -t postgres_data -CThe format (.gz) is detected automatically from the file extension.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -CThe latest backup found for each volume is selected. If backups were created at different times, a warning about possible inconsistency will appear in the logs.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -T 20250411_120000 -COnly backups with exactly this timestamp are restored. Volumes with no matching backup at that timestamp are silently skipped. If you explicitly filter with -v and a backup is missing for that volume, the command will fail.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -v postgres_data -v redis_data -T 20250411_120000 -Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -j 3 -T 20250411_120000 -C| Variable | Default | Description |
|---|---|---|
BACKUP_ROOT |
/backups |
Directory for backup files |
VOLUMES_ROOT |
/volumes |
Directory with mounted volumes |
GPG_ROOT |
/gpg |
Directory with GPG key files |
NO_COLOR |
— | Set to any value to disable colored log output |
| Variable | Description |
|---|---|
BACKUP_TIMESTAMP |
Override the backup timestamp (format: YYYYMMDD_HHMMSS) |
GPG_PUBLIC_KEY |
Public key content for asymmetric encryption |
GPG_PASSPHRASE |
Passphrase for symmetric encryption |
| Variable | Description |
|---|---|
GPG_PRIVATE_KEY |
Private key content for asymmetric decryption |
GPG_PASSPHRASE |
Passphrase for symmetric decryption or a passphrase-protected private key |
| Variable | Default | Description |
|---|---|---|
AWS_ACCESS_KEY_ID |
— | S3 access key |
AWS_SECRET_ACCESS_KEY |
— | S3 secret key |
AWS_SESSION_TOKEN |
— | Session token for temporary IAM credentials |
AWS_REGION |
us-east-1 |
S3 region |
AWS_ENDPOINT_URL |
— | Custom S3-compatible endpoint (MinIO, Yandex Cloud, DO Spaces, etc.) |
| Extension | Compression | Encryption |
|---|---|---|
.tar |
none | none |
.tar.gz |
gzip | none |
.tar.gpg |
none | GPG |
.tar.gz.gpg |
gzip | GPG |
The format during backup is determined by the -c, -e, -s flags.
During restore, the format is detected automatically from the file extension.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -v postgres_data -cResult: postgres_data_20250411_120000.tar.gz
The simplest option: one passphrase for both encryption and decryption.
Backup:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--env GPG_PASSPHRASE=my-secret-password \
ghcr.io/vansergen/vb backup -v postgres_data -c -sResult: postgres_data_20250411_120000.tar.gz.gpg
Restore:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--env GPG_PASSPHRASE=my-secret-password \
ghcr.io/vansergen/vb restore -i postgres_data_20250411_120000.tar.gz.gpg -t postgres_data -s -CBest for separating roles: backups are created with the public key; only the private key owner can decrypt.
Backup — only the public key is needed:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--volume ./gpg:/gpg \
ghcr.io/vansergen/vb backup -v postgres_data -c -e -k public.asc./gpg/public.asc must contain the GPG public key.
Restore — private key is required:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--volume ./gpg:/gpg \
ghcr.io/vansergen/vb restore \
-i postgres_data_20250411_120000.tar.gz.gpg \
-t postgres_data \
-k private.asc \
-CIf the private key is passphrase-protected:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--volume ./gpg:/gpg \
--env GPG_PASSPHRASE=key-passphrase \
ghcr.io/vansergen/vb restore \
-i postgres_data_20250411_120000.tar.gz.gpg \
-t postgres_data \
-k private.asc \
-CConvenient for CI/CD pipelines.
Backup:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--env GPG_PUBLIC_KEY="$(cat ./gpg/public.asc)" \
ghcr.io/vansergen/vb backup -v postgres_data -c -eRestore:
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
--env GPG_PRIVATE_KEY="$(cat ./gpg/private.asc)" \
--env GPG_PASSPHRASE=key-passphrase \
ghcr.io/vansergen/vb restore \
-i postgres_data_20250411_120000.tar.gz.gpg \
-t postgres_data \
-CS3 uploads and downloads use the MinIO client (mc).
This works with AWS S3, MinIO, Yandex Cloud Object Storage, DigitalOcean Spaces, and other S3-compatible endpoints.
Compatible storage: AWS S3, Yandex Cloud Object Storage, MinIO, DigitalOcean Spaces, and any S3-compatible endpoint.
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--env AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE \
--env AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
--env AWS_REGION=eu-central-1 \
ghcr.io/vansergen/vb backup -v postgres_data -c -b my-bucket -f daily/app1The object will be stored at:
https://s3.eu-central-1.amazonaws.com/my-bucket/daily/app1/postgres_data_20250411_120000.tar.gz
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--env AWS_ACCESS_KEY_ID=<KEY_ID> \
--env AWS_SECRET_ACCESS_KEY=<SECRET_KEY> \
--env AWS_REGION=ru-central1 \
--env AWS_ENDPOINT_URL=https://storage.yandexcloud.net \
ghcr.io/vansergen/vb backup -v postgres_data -c -b my-bucket -f daily/proddocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--env AWS_ACCESS_KEY_ID=minioadmin \
--env AWS_SECRET_ACCESS_KEY=minioadmin \
--env AWS_REGION=us-east-1 \
--env AWS_ENDPOINT_URL=http://minio:9000 \
ghcr.io/vansergen/vb backup -v postgres_data -c -b my-bucketdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--env AWS_ACCESS_KEY_ID=<KEY_ID> \
--env AWS_SECRET_ACCESS_KEY=<SECRET_KEY> \
--env AWS_REGION=eu-central-1 \
--env GPG_PASSPHRASE=my-secret \
ghcr.io/vansergen/vb backup -v postgres_data -c -s -b my-bucket -f daily/app1docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--env AWS_ACCESS_KEY_ID=<KEY_ID> \
--env AWS_SECRET_ACCESS_KEY=<SECRET_KEY> \
--env AWS_REGION=eu-central-1 \
ghcr.io/vansergen/vb restore \
-i s3://my-bucket/daily/app1/postgres_data_20250411_120000.tar.gz \
-t postgres_data \
-Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--env AWS_ACCESS_KEY_ID=<KEY_ID> \
--env AWS_SECRET_ACCESS_KEY=<SECRET_KEY> \
--env AWS_REGION=eu-central-1 \
--env GPG_PASSPHRASE=my-secret \
ghcr.io/vansergen/vb restore \
-i s3://my-bucket/daily/app1/postgres_data_20250411_120000.tar.gz.gpg \
-t postgres_data \
-s \
-Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -c -T 20250411_120000Files created in ./backups:
postgres_data_20250411_120000.tar.gz
postgres_data_20250411_120000.tar.gz.sha256
redis_data_20250411_120000.tar.gz
redis_data_20250411_120000.tar.gz.sha256
uploads_20250411_120000.tar.gz
uploads_20250411_120000.tar.gz.sha256
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -T 20250411_120000 -Cdocker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume uploads:/volumes/uploads \
--env AWS_ACCESS_KEY_ID=<KEY_ID> \
--env AWS_SECRET_ACCESS_KEY=<SECRET_KEY> \
--env AWS_REGION=eu-central-1 \
--env GPG_PASSPHRASE=my-secret \
ghcr.io/vansergen/vb backup -a -c -s -b my-bucket -f daily/prod -j 2The -a mode backs up everything mounted under /volumes, but the container only sees what you mount in. The patterns below build the mount list dynamically from docker volume ls.
docker run --rm \
$(docker volume ls -q | sed 's|.*|--volume &:/volumes/&|') \
--volume "$(pwd)/backups:/backups" \
ghcr.io/vansergen/vb backup -a -cdocker volume ls -q lists every named volume on the host; sed expands each name into a --volume <name>:/volumes/<name> pair.
Better suited for cron and scripts: skips anonymous volumes (64-char hex names), handles names with special characters, and accepts a shared timestamp for atomic per-snapshot restore.
#!/usr/bin/env bash
set -euo pipefail
mounts=()
while IFS= read -r vol; do
# skip anonymous volumes (Docker autogenerates 64-char hex names)
[[ "$vol" =~ ^[a-f0-9]{64}$ ]] && continue
mounts+=(--volume "$vol:/volumes/$vol")
done < <(docker volume ls -q)
docker run --rm \
"${mounts[@]}" \
--volume "$(pwd)/backups:/backups" \
ghcr.io/vansergen/vb backup -a -c -j 2 -T "$(date -u +%Y%m%d_%H%M%S)"Use Docker labels to opt volumes in explicitly — recommended for production so the script never picks up volumes you didn't intend to back up:
docker volume ls -q --filter label=backup=true# in your application's compose.yaml
volumes:
postgres_data:
labels:
backup: "true"Declare a one-shot backup service alongside your application and reuse the same named volumes. The profiles: [backup] line keeps the service out of regular docker compose up.
services:
postgres:
image: postgres:17
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
volumes:
- redis_data:/data
backup:
image: ghcr.io/vansergen/vb
profiles: [backup]
command: backup -a -c -s -j 2
environment:
GPG_PASSPHRASE: ${GPG_PASSPHRASE}
volumes:
- postgres_data:/volumes/postgres_data
- redis_data:/volumes/redis_data
- ./backups:/backups
volumes:
postgres_data:
redis_data:Run it on demand:
GPG_PASSPHRASE=secret docker compose run --rm backupTo restore the same snapshot, replace command: with restore -a -T <timestamp> -C (or run a sibling restore service the same way).
Dry run validates parameters and prints what would be done, without creating files or modifying volumes.
# Check a single volume backup
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -v postgres_data -c -n
# Check all volumes backup
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb backup -a -c -n
# Check a single volume restore
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -i postgres_data_20250411_120000.tar.gz -t postgres_data -n
# Check all volumes restore
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -a -T 20250411_120000 -nCreate a run-backup.sh wrapper:
#!/usr/bin/env bash
set -euo pipefail
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume redis_data:/volumes/redis_data \
--volume /opt/backups:/backups \
--env AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}" \
--env AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}" \
--env AWS_REGION=eu-central-1 \
--env GPG_PASSPHRASE="${GPG_PASSPHRASE}" \
ghcr.io/vansergen/vb backup -a -c -s -b my-bucket -f "daily/$(hostname)"Add to crontab -e:
0 3 * * * /opt/scripts/run-backup.sh >> /var/log/docker-volume-backup.log 2>&10 3 * * * docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume /opt/backups:/backups \
ghcr.io/vansergen/vb backup -v postgres_data -c \
>> /var/log/docker-volume-backup.log 2>&1The published image supports linux/amd64, linux/arm64/v8, and linux/ppc64le. 32-bit ARM platforms (linux/arm/v6, linux/arm/v7, e.g. Raspberry Pi Zero / 1 / 2 / 3 running a 32-bit OS) are no longer published, because the MinIO client (mc) bundled in the image no longer ships 32-bit ARM builds. Use a 64-bit OS on supported ARM boards.
Before restoring data for databases or other stateful services, stop their containers first:
docker stop my-postgres
docker run --rm \
--volume postgres_data:/volumes/postgres_data \
--volume ./backups:/backups \
ghcr.io/vansergen/vb restore -i postgres_data_20250411_120000.tar.gz -t postgres_data -C
docker start my-postgresFile-level volume backups work well for static files, uploads, and general state directories.
For PostgreSQL, MySQL, MongoDB, and Redis, logical dumps (pg_dump, mysqldump, mongodump) are more reliable — they produce a consistent snapshot without stopping the service.
When you need to restore several volumes as a coherent set, ensure they are all backed up with the same timestamp:
docker run --rm ... ghcr.io/vansergen/vb backup -a -c -T 20250411_120000This guarantees that restore picks exactly those files:
docker run --rm ... ghcr.io/vansergen/vb restore -a -T 20250411_120000 -CIf your volumes contain personal data, credentials, tokens, or business-critical data, use -s (symmetric GPG) or -e (asymmetric GPG).
Having a backup file does not guarantee a successful restore. Periodically restore to a separate test volume and verify that the application starts correctly.
A good strategy: keep a local copy and an offsite copy in S3 or another object storage.
High -j with compression can saturate CPU, disk, and network simultaneously. Start with -j 1 and increase gradually.
mc pipe handles upload from a stream and uses multipart upload internally when needed. It works with AWS S3, MinIO, Yandex Cloud Object Storage, DigitalOcean Spaces, and other S3-compatible endpoints.
Before uploading, the backup script checks that the target S3 object does not already exist and refuses to proceed otherwise (a stat error that is not a confirmed "not found" is also treated as fatal — fail-closed). This handles accidental reruns and most ambiguous failures, but it does not give strict atomicity: two parallel backups racing on the same key may both pass the existence check, and S3-side write ordering then decides the winner. For production, enable bucket-side object versioning (and ideally retention / object lock) so that even a same-key upload preserves the previous recovery point as a prior version. AWS S3, Yandex Cloud Object Storage, MinIO, and most S3-compatible stores expose this as a bucket-level setting.
The volume is not mounted into the container. Add --volume <vol-name>:/volumes/<vol-name> to your docker run command.
A file with that name already exists in BACKUP_ROOT. Use a different timestamp (-T) or remove the existing file.
The target volume already contains data. Add -C to clear it before restoring.
The mc client could not connect to the S3 endpoint. Check that AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_ENDPOINT_URL (if using a custom store) are set correctly and that the endpoint is reachable from inside the container.
You are using -s (symmetric encryption/decryption) but did not provide the passphrase. Add --env GPG_PASSPHRASE=<passphrase>.
For asymmetric decryption, the script looks for a private key at /gpg/private.gpg by default. Either mount --volume ./gpg:/gpg with the key file present, specify the path with -k, or pass the key content via GPG_PRIVATE_KEY.
The directory is empty or the files do not match the expected naming pattern:
<VOLUME>_<YYYYMMDD_HHMMSS>.tar[.gz][.gpg]
In restore -a -v <name> mode, the specified volume has no matching backup file in BACKUP_ROOT. Check the available files and the volume name.
This is a warning, not an error: backups for different volumes have different timestamps. For a consistent set, use -T <timestamp> to pin all volumes to the same snapshot.
The scripts create a temporary GNUPGHOME with correct permissions automatically. If GPG still fails, pull the latest image: docker pull ghcr.io/vansergen/vb.
At the start of each restore, any .restore-in-progress.* directories left behind by a previously interrupted restore are automatically removed from the target volume. Do not use this prefix for your own data inside a volume.
Can I use bind mounts instead of Docker volumes?
Yes. The scripts work with any directory mounted under VOLUMES_ROOT. Bind mounts are treated identically to named Docker volumes.
Do I need to stop containers to take a backup?
Not for static files. For actively writing databases (PostgreSQL, MySQL, Redis), it depends on your consistency requirements. A file-level backup of a running database may be file-intact but logically inconsistent.
Why does -a restore only work with local files, not S3?
The -a mode automatically scans BACKUP_ROOT and matches filenames to volume names. S3 does not provide local file scanning. To restore from S3, use single mode with an explicit source: restore -i s3://....
Symmetric (-s) vs. asymmetric (-e) encryption — which should I use?
Symmetric (-s) is simpler: one passphrase, easy to automate.
Asymmetric (-e) is better for role separation: backups can be created with only the public key; only the private key owner can decrypt.
Can I use Podman instead of Docker?
Yes. Replace docker run with podman run — the syntax is identical.
What if I need a true crash-consistent snapshot of all volumes?
These scripts operate at the file-archive level. For a crash-consistent snapshot, consider:
- Filesystem snapshots (LVM, ZFS, btrfs)
- Storage-level snapshots (cloud disks)
- App-aware backup tools for stateful services (
pg_dump, etc.)