How to Back Up a Database Running in a Docker Container: MySQL, PostgreSQL, and MongoDB Backup Guide

How to Back Up a Database Running in a Docker Container

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 rm or docker compose down -v
  • Host disk failure
  • Data corruption from a bad migration or bad application logic
  • Human error (dropped tables, bad DELETE statements)
  • 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:

  1. 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.
  2. 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 .env file that’s excluded from git, or use Docker secrets (docker secret create on Swarm).
  • Kubernetes: always use a Secret object referenced via secretKeyRef, 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_dump from 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

Total
0
Shares

Leave a Reply

Previous Post
How to Run a WordPress Blog Using Two Linked Docker Containers

How to Run a WordPress Blog Using Two Linked Docker Containers: MySQL and WordPress Setup

Next Post
How to Share Data in Your Docker Host with Containers

How to Share Data in Your Docker Host with Containers: Volume Mounts and Persistent Storage Setup

Related Posts