Jump to: volumes or bind mounts, database dumps, restoring or scheduling backups.
Before you start
You need a working Docker Engine and Compose plugin; the Docker Installation Guide covers both. The examples back up the Miniflux and PostgreSQL stack from the Docker Compose guide, but the commands work for any project. Replace miniflux with your project name and db-data with your volume name.
One rule shapes everything that follows: a backup you haven't restored is a guess. The last section restores into a throwaway project so you can prove the files are usable before you need them.
docker volume ls # every volume on the host
docker compose -p miniflux ps # what the project is running now
Where container data actually lives
A container's own filesystem is a writable layer on top of its image. It disappears when the container is removed, and Compose removes and recreates containers whenever you change their settings or pull a newer image. Anything you want to keep has to live outside that layer, in one of two places:
- Named volumes are created and managed by Docker. On a default Linux install their data sits under
/var/lib/docker/volumes/name/_data, owned by root. - Bind mounts map a host directory you choose, such as
./data, into the container.
To see which a running container uses:
docker inspect --format '{{range .Mounts}}{{.Type}} {{.Name}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' miniflux-db-1
Any path the image declares as a VOLUME but that you didn't mount gets an anonymous volume with a random 64-character name. Those are easy to lose: docker compose down -v and docker volume prune both remove them. If docker inspect shows a volume with a long hexadecimal name, give that path a named volume in your Compose file.
Volumes or bind mounts
| Property | Named volume | Bind mount |
|---|---|---|
| Location | Docker chooses, under /var/lib/docker | You choose |
| First start with an empty mount | Docker copies the image's files at that path into the volume | The host directory hides whatever the image had there |
| File ownership | Docker sets it up from the image | Whatever is on the host; often a permissions mismatch |
| Backing up | Needs a helper container or root access | Any backup tool that can read the directory |
| Good for | Databases and app state you don't edit by hand | Config files you edit, media libraries, files shared with the host |
Both are fine. What matters is that you know which one each service uses and that both are in your backup plan.
Why copying a live database is risky
A database writes to several files at once and keeps recent changes in memory and in its write-ahead log. Copying its data directory while it's running can capture those files at different moments, producing a backup that won't start, or one that starts with silently missing rows. There are two safe approaches:
- A logical dump made by the database's own tool while it runs, such as
pg_dumpormariadb-dump. It's consistent, portable across versions and usually small. Use this for databases. - A file copy with the service stopped. Brief downtime, but it works for anything, including apps that keep state in SQLite or plain files.
Dump a database from its container
Run the dump tool inside the database container with docker compose exec, and send its output to a file on the host. -T turns off the pseudo-terminal so the binary output isn't mangled.
mkdir -p ~/backups/miniflux
cd ~/stacks/miniflux
docker compose exec -T db pg_dump -U miniflux -d miniflux --format=custom \
> ~/backups/miniflux/miniflux-$(date +%F).dump
docker compose exec -T db sh -c 'exec mariadb-dump --single-transaction -uroot -p"$MARIADB_ROOT_PASSWORD" --all-databases' \
> ~/backups/app/app-$(date +%F).sql
For PostgreSQL, --format=custom produces a compressed archive that pg_restore can restore selectively; the pg_dump documentation describes the other formats. For MariaDB, --single-transaction takes a consistent snapshot of InnoDB tables without locking them. The single quotes make the shell inside the container expand $MARIADB_ROOT_PASSWORD from its own environment, so the password never appears in your shell history.
Check that the file isn't empty and that it can be read:
ls -lh ~/backups/miniflux/
docker compose exec -T db pg_restore --list < ~/backups/miniflux/miniflux-$(date +%F).dump | head
A dump of a few hundred bytes usually means the command failed and wrote an error message into the file. Open it with head to see.
Archive a volume with a helper container
Named volumes are owned by root inside /var/lib/docker. Instead of reading that directory directly, start a short-lived container that mounts the volume read-only alongside a host directory, and let it write a tar archive:
cd ~/stacks/miniflux
docker compose stop db
docker run --rm \
-v miniflux_db-data:/source:ro \
-v ~/backups/miniflux:/backup \
alpine:3 \
tar -czf /backup/db-data-$(date +%F).tar.gz -C /source .
docker compose start db
Note the volume name. Compose prefixes volumes with the project name, so db-data in the file is miniflux_db-data to Docker. docker volume ls shows the real names. The $(date +%F) is expanded by your shell before the container starts, which is what you want.
The archive keeps numeric file ownership and permissions, so a restore puts the files back exactly as the database expects them. List the contents to check the archive:
tar -tzvf ~/backups/miniflux/db-data-$(date +%F).tar.gz | head
For a bind mount, you don't need the helper container. Stop the service and archive the host directory directly:
docker compose stop app
sudo tar -czf ~/backups/app/data-$(date +%F).tar.gz -C ~/stacks/app data
docker compose start app
What else belongs in the backup
Data on its own isn't enough to rebuild a service. Back up the project directory as well:
compose.yamland any override files, so you know exactly which image versions and settings produced the data..env, which holds the passwords the data was created with. Store this copy encrypted or in a password manager, because it contains secrets.- Any config files bind-mounted into containers.
You don't need to back up images. They can be pulled again, as long as the tags in compose.yaml are specific enough to get the same versions back.
Restore and test
Practise restoring into a separate project, so a mistake can't touch the live data. Copy the project directory, give the copy its own project name and port, and restore into it.
cp -r ~/stacks/miniflux ~/stacks/miniflux-restore-test
cd ~/stacks/miniflux-restore-test
sed -i 's/^name: miniflux$/name: miniflux-restore-test/' compose.yaml
sed -i 's/127.0.0.1:8080:8080/127.0.0.1:8090:8080/' compose.yaml
docker compose config | grep -E '^name:|published'
From a PostgreSQL dump
Start only the database so it creates an empty one, then load the dump into it:
docker compose up -d db
docker compose exec -T db pg_restore -U miniflux -d miniflux --clean --if-exists --no-owner \
< ~/backups/miniflux/miniflux-2026-10-19.dump
docker compose up -d
From a volume archive
Create the empty volume, unpack the archive into it, then start the stack:
docker volume create miniflux-restore-test_db-data
docker run --rm \
-v miniflux-restore-test_db-data:/target \
-v ~/backups/miniflux:/backup:ro \
alpine:3 \
sh -c 'cd /target && tar -xzf /backup/db-data-2026-10-19.tar.gz'
docker compose up -d
Then check that the restored copy actually works, not just that it started:
docker compose ps
docker compose exec db psql -U miniflux -d miniflux -c 'SELECT count(*) FROM feeds;'
curl -sI http://127.0.0.1:8090/healthcheck
Log in on port 8090 and confirm your feeds are there. Then remove the test project and its volume:
docker compose down -v
cd ~ && rm -rf ~/stacks/miniflux-restore-test
A dump also restores into a newer PostgreSQL version, which is how you move between major versions: dump from the old version, change the image tag, start on an empty volume and restore. A volume archive only works with the same major version that created it.
Scheduling backups
Put the commands in a script so every run is the same:
#!/usr/bin/env bash
set -euo pipefail
project_dir=/home/deploy/stacks/miniflux
dest=/home/deploy/backups/miniflux
keep_days=14
stamp=$(date +%F-%H%M)
mkdir -p "$dest"
cd "$project_dir"
docker compose exec -T db pg_dump -U miniflux -d miniflux --format=custom > "$dest/miniflux-$stamp.dump.partial"
mv "$dest/miniflux-$stamp.dump.partial" "$dest/miniflux-$stamp.dump"
tar -czf "$dest/project-$stamp.tar.gz" -C "$project_dir" compose.yaml .env
find "$dest" -name 'miniflux-*.dump' -mtime +"$keep_days" -delete
find "$dest" -name 'project-*.tar.gz' -mtime +"$keep_days" -delete
set -euo pipefail stops the script at the first failure, including a failed pg_dump inside the pipeline. Writing to a .partial file and renaming it afterwards means a failed run never leaves a half-written file that looks like a good backup. Replace the paths with your own and make the script executable with sudo chmod 750.
Run it daily with a systemd timer, which logs every run to the journal. The systemd services guide explains the unit files in detail.
[Unit]
Description=Back up the Miniflux database
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
User=deploy
ExecStart=/usr/local/bin/backup-miniflux.sh
[Unit]
Description=Daily Miniflux backup
[Timer]
OnCalendar=*-*-* 03:15:00
Persistent=true
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup-miniflux.timer
sudo systemctl start backup-miniflux.service # run once now to test it
journalctl -u backup-miniflux.service -n 20
The deploy user must be in the docker group for docker compose exec to work. Persistent=true runs a missed backup at the next boot if the machine was off at 03:15.
How many backups to keep
Keeping only the latest backup protects you from a dead disk, but not from a mistake you notice a week later. If a bad migration or a deleted feed went unnoticed for three days, last night's backup has the problem in it too. A simple schedule covers both cases:
| Copies | Covers |
|---|---|
| One per day for 7 to 14 days | Recent mistakes and failed upgrades |
| One per week for 4 to 8 weeks | Problems noticed late |
| One per month for 6 to 12 months | Slow corruption and old data you deleted on purpose and later wanted back |
The script above keeps 14 days of daily copies, which is a sensible start for a small service. Tools such as restic express the full schedule directly, for example with restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --prune. Whatever you keep, watch the size: du -sh ~/backups/* once a month catches a backup that has suddenly grown, or shrunk to almost nothing because a dump started failing.
Get a copy off the machine
Backups on the same disk as the data protect against mistakes, not against a failed disk, a stolen machine or ransomware. Keep at least one copy somewhere else: another machine on your network, a NAS, or object storage. A deduplicating tool such as restic encrypts the backups before they leave the host and stores repeated runs efficiently. Whatever you use, repeat the restore test from the off-site copy every few months.
Troubleshooting
The volume is empty after docker compose up in a new directory
The project name changed, so Compose created a new volume instead of using the old one. docker volume ls will show both. Set name: at the top of compose.yaml to the original project name, or move the data across with the helper-container method.
pg_restore: error: input file appears to be a text format dump
The dump was made without --format=custom, so it's plain SQL. Restore it with psql instead: docker compose exec -T db psql -U miniflux -d miniflux < file.sql.
The restored database refuses to start: database files are incompatible with server
You restored a volume archive into a different PostgreSQL major version. Use the image tag the archive was made with, or restore from a logical dump instead.
Permission errors on a bind mount after a restore
Unpack archives with sudo tar -xzf so numeric owners are kept, and check the UID the container runs as with docker compose exec app id.
Next steps
- Docker Compose: Multi-Container Apps: the stack backed up here.
- TrueNAS Storage Setup: a target for off-site copies with snapshots.
- Docker's volumes documentation, including its own backup and restore example.