I’ve had exactly one moment of genuine panic in my time running containerized databases: a docker compose down -v run by mistake on a staging environment that turned out to have real customer data in it. Nothing was lost that time because a backup job had run an hour earlier, but it was close enough that I now treat database backups as non-negotiable infrastructure, not an afterthought. This guide covers backup and restore for the three databases I run most often in containers — MySQL, PostgreSQL, and MongoDB — along with automation, offsite storage, and Kubernetes-native scheduling.
Why Containerized Databases Need a Deliberate Backup Strategy
A named volume protects your data from container recreation, but it does not protect you from:
- Accidental
docker volume rmordocker compose down -v - Host disk failure
- Data corruption from a bad migration or bad application logic
- Human error (dropped tables, bad
DELETEstatements) - Ransomware or malicious access
A volume is persistence, not a backup. A backup is a separate, independent copy of your data, ideally stored somewhere other than the same disk the volume lives on. With that distinction in mind, let’s go through each database.
General Backup Approaches
There are two broad strategies for any containerized database:
- Logical backups — using the database’s own dump tool (
mysqldump,pg_dump,mongodump) to export data as a portable file. Slower for very large datasets, but portable across versions and easy to restore selectively. - Physical/volume backups — snapshotting the underlying data files directly (tar of the volume, LVM snapshot, cloud disk snapshot). Faster for huge datasets, but tied to matching database versions and storage layouts.
I generally default to logical backups unless the dataset is large enough that dump time becomes impractical, in which case I add periodic physical snapshots as a second layer.
MySQL Backup
Logical Backup with mysqldump
Assuming a running container named mysql-db:
docker run -d --name mysql-db \
-e MYSQL_ROOT_PASSWORD=changeme \
-e MYSQL_DATABASE=appdb \
-v mysql-data:/var/lib/mysql \
-p 3306:3306 \
mysql:8.4
Run the dump using docker exec, streaming the output to the host:
docker exec mysql-db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" --all-databases' > mysql-backup-$(date +%F).sql
Expected output: a .sql file on the host, starting with something like:
-- MySQL dump 10.13 Distrib 8.4.0, for Linux (x86_64)
--
-- Host: localhost Database:
-- ------------------------------------------------------
For a single database instead of all:
docker exec mysql-db sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" appdb' > appdb-backup-$(date +%F).sql
MySQL Restore
docker exec -i mysql-db sh -c 'exec mysql -uroot -p"$MYSQL_ROOT_PASSWORD" appdb' < appdb-backup-2026-07-29.sql
Verify:
docker exec mysql-db mysql -uroot -p"changeme" -e "SHOW TABLES;" appdb
Expected output:
+------------------+
| Tables_in_appdb |
+------------------+
| users |
| orders |
+------------------+
PostgreSQL Backup
Logical Backup with pg_dump
docker run -d --name postgres-db \
-e POSTGRES_PASSWORD=changeme \
-e POSTGRES_DB=appdb \
-v pg-data:/var/lib/postgresql/data \
-p 5432:5432 \
postgres:16
docker exec -t postgres-db pg_dump -U postgres appdb > appdb-backup-$(date +%F).sql
For all databases on the instance, including roles:
docker exec -t postgres-db pg_dumpall -U postgres > full-cluster-backup-$(date +%F).sql
For a compressed, restore-flexible custom format (recommended for anything beyond trivial size):
docker exec -t postgres-db pg_dump -U postgres -Fc appdb > appdb-backup-$(date +%F).dump
PostgreSQL Restore
Plain SQL dump:
docker exec -i postgres-db psql -U postgres -d appdb < appdb-backup-2026-07-29.sql
Custom-format dump (allows parallel restore, selective table restore):
docker cp appdb-backup-2026-07-29.dump postgres-db:/tmp/backup.dump
docker exec -t postgres-db pg_restore -U postgres -d appdb --clean /tmp/backup.dump
Verify:
docker exec postgres-db psql -U postgres -d appdb -c "\dt"
MongoDB Backup
Logical Backup with mongodump
docker run -d --name mongo-db \
-v mongo-data:/data/db \
-p 27017:27017 \
mongo:7
docker exec mongo-db mongodump --archive=/tmp/backup.archive --gzip
docker cp mongo-db:/tmp/backup.archive ./mongo-backup-$(date +%F).archive
Expected output from mongodump:
writing admin.system.version to archive on stdout
done dumping admin.system.version (1 document)
writing appdb.users to archive on stdout
done dumping appdb.users (152 documents)
MongoDB Restore
docker cp mongo-backup-2026-07-29.archive mongo-db:/tmp/restore.archive
docker exec mongo-db mongorestore --archive=/tmp/restore.archive --gzip --drop
The --drop flag drops existing collections before restoring, which is what you want for a clean point-in-time restore — omit it if you’re merging data instead.
Verify:
docker exec mongo-db mongosh --quiet --eval "db.getSiblingDB('appdb').users.countDocuments()"
Expected output:
152
Automating Backups with a Cron Container
Rather than remembering to run these commands manually, I run a dedicated backup container on a schedule. Here’s a Compose setup with a sidecar that backs up PostgreSQL nightly:
version: "3.9"
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: appdb
volumes:
- pg-data:/var/lib/postgresql/data
backup:
image: postgres:16
depends_on:
- db
environment:
PGPASSWORD: changeme
volumes:
- ./backups:/backups
entrypoint: >
sh -c "while true; do
pg_dump -h db -U postgres -Fc appdb > /backups/appdb-$$(date +%F-%H%M).dump;
find /backups -type f -mtime +7 -delete;
sleep 86400;
done"
volumes:
pg-data:
This backup container dumps nightly, writes to a host-mounted ./backups directory (deliberately a bind mount, not a named volume, so backups land somewhere you can see and copy off-box directly), and prunes anything older than 7 days.
Backing Up the Raw Volume as a Fallback Layer
In addition to logical dumps, I like having an occasional raw snapshot of the data directory — useful for fast disaster recovery when dump/restore time would be too slow:
docker run --rm \
-v pg-data:/data \
-v $(pwd)/volume-backups:/backup \
busybox tar czf /backup/pg-data-$(date +%F).tar.gz -C /data .
Restoring this requires the destination database to be the same major version, since it’s a binary data directory copy, not a portable dump — a caveat worth remembering before relying on it exclusively.
Shipping Backups Offsite
A backup that lives on the same disk as the database it protects doesn’t protect you from disk or host failure. I typically pipe the compressed backup straight to object storage using rclone in the same container, or a follow-up job:
docker run --rm \
-v $(pwd)/backups:/backups \
rclone/rclone:latest \
copy /backups remote:my-db-backups --config /root/.config/rclone/rclone.conf
Or with the AWS CLI directly to S3:
docker run --rm \
-v $(pwd)/backups:/backups \
-e AWS_ACCESS_KEY_ID \
-e AWS_SECRET_ACCESS_KEY \
amazon/aws-cli s3 cp /backups s3://my-db-backups/ --recursive
Kubernetes: Scheduled Backups with CronJob
On Kubernetes, the equivalent of a cron-based backup container is a native CronJob:
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: pg-backup
image: postgres:16
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: postgres-credentials
key: password
command:
- "sh"
- "-c"
- "pg_dump -h postgres -U postgres -Fc appdb > /backups/appdb-$(date +%F).dump"
volumeMounts:
- name: backup-storage
mountPath: /backups
restartPolicy: OnFailure
volumes:
- name: backup-storage
persistentVolumeClaim:
claimName: backup-pvc
Check job history:
kubectl get cronjob postgres-backup
kubectl get jobs --selector=job-name=postgres-backup
Security: Handling Credentials Safely
Never hardcode passwords into backup scripts or Compose files committed to version control. Practical patterns I use:
- Docker: pass credentials via environment variables sourced from a
.envfile that’s excluded from git, or use Docker secrets (docker secret createon Swarm). - Kubernetes: always use a
Secretobject referenced viasecretKeyRef, never a plain env value in the manifest. - Encrypt backup archives before shipping them offsite if they contain sensitive data:
gpg --symmetric --cipher-algo AES256 appdb-backup-2026-07-29.dump
Best Practices
- Automate backups — manual backups get forgotten exactly when you need them most.
- Store backups off the host running the database, ideally in a different physical location or cloud region.
- Test restores regularly. A backup you’ve never restored from is a hypothesis, not a guarantee.
- Keep a retention policy (daily for a week, weekly for a month, monthly for a year is a reasonable starting point) rather than keeping everything forever or deleting too aggressively.
- Version-match your restore tooling to your backup tooling — restoring a
pg_dumpfrom Postgres 16 into a Postgres 12 instance can fail on newer SQL features. - Log every backup job’s success/failure and alert on failures, not just successes.
Troubleshooting
mysqldump fails with “Access denied” Confirm the user has the SELECT, LOCK TABLES, and SHOW VIEW privileges at minimum; for --all-databases, use the root user or a user with global privileges.
pg_dump hangs on a large table Check for long-running transactions holding locks:
docker exec postgres-db psql -U postgres -c "SELECT pid, state, query FROM pg_stat_activity WHERE state != 'idle';"
mongorestore fails with “namespace exists” Add --drop to replace existing collections, or restore into a differently named database first to inspect before overwriting production data.
Backup file is 0 bytes Usually means the dump command failed silently inside a script — always check the exit code and pipe stderr to a log file rather than discarding it.
Monitoring Backup Jobs
For Docker Compose setups, a simple approach is having the backup script write a timestamp file after success, and monitoring its age externally:
find /backups -name "*.dump" -mtime -1 | wc -l
If that returns 0, no successful backup ran in the last 24 hours — wire this check into your existing monitoring (Prometheus node exporter’s textfile collector works well for this) so a missed backup triggers an alert rather than going unnoticed.
On Kubernetes, kubectl get jobs combined with an alert on CronJob failure status (via kube-state-metrics and Prometheus) gives you the same visibility natively.
Summary
Volumes keep your containerized database’s data alive across restarts, but only a real backup — a separate, tested, offsite copy — protects you from the mistakes and failures that eventually happen to everyone. mysqldump, pg_dump/pg_dumpall, and mongodump give you portable, restorable exports for MySQL, PostgreSQL, and MongoDB respectively; wrap them in a scheduled job (a Compose sidecar or a Kubernetes CronJob), ship the results offsite, and — most importantly — actually practice restoring from them before you need to do it for real.
References
- MySQL Docs — mysqldump — A Database Backup Program
- PostgreSQL Docs — pg_dump
- PostgreSQL Docs — pg_restore
- MongoDB Docs — mongodump
- MongoDB Docs — mongorestore
- Docker Docs — Manage data in Docker
- Kubernetes Docs — CronJob
- Kubernetes Docs — Secrets
