Automate PostgreSQL Backups in Docker Containers
Build PostgreSQL backups for Docker with pg_dump, S3-compatible storage, retention, monitoring, and restore tests that exercise the recovery path.
How to Back Up PostgreSQL in Docker Automatically
You're running PostgreSQL in Docker. Your data lives in a volume. If that volume is your only copy, a disk failure, an accidental volume removal, or a bad migration can leave you with nothing clean to recover.
A Docker volume keeps data across container restarts. It does not create a second copy, preserve an earlier point in time, or move the data away from the server it depends on.
Answer first: Automate PostgreSQL backups in Docker with a separate backup container that runs pg_dump on a schedule, compresses the result, and uploads it to S3-compatible storage. Set retention in both local and object storage, then restore a backup into a temporary database on a regular schedule. If you do not want to maintain that machinery, Temps has built-in backup scheduling, S3-compatible destinations, retention, and restore workflows.
The hard part is not producing a dump file. It is keeping that file outside the database's failure path and proving that you can restore it.
This guide covers why Docker volumes aren't backups, the three PostgreSQL backup strategies you should know, the sidecar pattern for automated dumps, and a step-by-step Docker Compose setup that runs backups on a schedule with retention and off-site upload.
TL;DR: Docker volumes are persistent storage, not backups. Run
pg_dumpfrom a separate container, upload the result to S3-compatible storage outside the database server, apply retention at both destinations, and test the full download-and-restore path.
Temps vs DIY: PostgreSQL Backup Comparison
| Capability | Temps (self-hosted) | DIY sidecar |
|---|---|---|
| Scheduled backups | Configure a cron expression | Build and operate the scheduler |
| PostgreSQL engine | WAL-G when supported; pg_dumpall fallback | Choose and maintain pg_dump, pgBackRest, Barman, or WAL-G |
| S3-compatible destination | Configure a source in Temps | Build upload and credential handling |
| Retention | Per schedule, with application-side cleanup and lifecycle rules when the provider supports them | Configure local cleanup and bucket lifecycle separately |
| Restore | Start and track a restore from Temps | Build and test the restore scripts |
| Failure visibility | Tracks overdue schedules and stalled jobs | Add monitoring and notifications |
| Scope | Target selected services, all services, and optionally the Temps control plane | Encode the target list yourself |
| Software license | Apache 2.0 | Depends on the tools you choose |
Why Aren't Docker Volumes Backups?
Docker volumes persist independently of a container, but they still depend on storage managed by the Docker host. A second volume on that same host may protect you from deleting the database volume; it does not protect you from losing the host or its storage.
Same Disk, Same Machine
Your PostgreSQL container writes to Docker-managed storage on the host. If the host or its storage fails, a backup kept only on that host can disappear with the live database.
Put the recoverable copy in a destination with the separation your risk model needs: commonly another provider, account, or region. S3 compatibility describes the API, not the independence of the storage, so check where the destination actually runs.
The docker compose down -v Problem
This one command deletes every named volume in your Compose stack:
# This removes the Compose project's named and anonymous volumes.
docker compose down -v
The -v / --volumes flag removes named volumes declared by the Compose project and anonymous volumes attached to its containers. If your database volume is in that set, Compose removes it with the stack.
That may be exactly what you want in development. It is a destructive production operation, and a remote backup gives you a recovery path when it happens.
No Point-in-Time Recovery
A Docker volume gives you the current state of the database. That's it. If a bad migration ran two hours ago and corrupted your user table, the volume already contains the corrupted data. You can't rewind to the state before the migration.
Point-in-time recovery (PITR) combines a base backup with a continuous sequence of archived WAL files. Docker volumes do not enable that automatically. With periodic logical dumps, your recovery point is the last successful backup; with working WAL archiving, you can choose a point after the base backup within the WAL history you retained.
What Are the PostgreSQL Backup Strategies?
PostgreSQL documents three broad approaches with different trade-offs in portability, operational complexity, and recovery granularity: SQL dumps, file-system-level backups, and continuous archiving with PITR.
pg_dump: Logical Backups
pg_dump creates a logical representation of your database — SQL statements or a custom-format archive that can recreate tables, indexes, and data. It's the most portable option.
Strengths:
- Can generally be restored into newer PostgreSQL versions
- Can back up individual databases or tables
- Output is human-readable (in SQL format) or compressed (custom format)
- Doesn't require filesystem access — connects over the network
Limitations:
- Can take much longer than physical methods for large databases
- Creates a snapshot at one point in time — no continuous protection
- Does not block ordinary readers or writers, but a long-running dump can still affect maintenance and can conflict with schema changes
Start with pg_dump when you value a portable logical backup and can complete the dump inside your backup window. Measure it against your own database rather than choosing from a size threshold.
pg_basebackup: Physical Backups
pg_basebackup creates a physical copy of the PostgreSQL cluster. It copies the files needed to start another server rather than serializing schema and rows as SQL.
Strengths:
- Captures the complete cluster, including all databases and roles
- Foundation for setting up streaming replication
Limitations:
- Can only restore to the exact same PostgreSQL major version
- Requires filesystem-level access or replication protocol
- Is tied much more closely to the server's physical format than a logical dump
In Docker, pg_basebackup requires replication access and a restore procedure designed for a complete cluster. Use it when that recovery model fits your requirements, not simply because the database crossed an arbitrary size.
WAL Archiving: Continuous Protection
Write-Ahead Log (WAL) archiving continuously copies completed WAL segments away from the database. Combined with a base backup and an unbroken WAL sequence, it enables PITR to a chosen recovery target.
Strengths:
- Can provide a much smaller RPO than periodic dumps when archiving is healthy
- Can recover to a chosen point covered by the retained base backup and WAL sequence
- Foundation for production-grade disaster recovery
Limitations:
- More complex to configure and maintain
- Requires continuous archive storage that grows with write volume
- Restore process is slower and more involved
Tools such as pgBackRest, Barman, and WAL-G automate parts of this workflow. Whichever tool you choose, monitor the archive continuously: one missing required WAL segment can break recovery beyond that point.
Comparison Table
| Method | Backup shape | Portability | Recovery point | Operational complexity |
|---|---|---|---|---|
pg_dump | Logical SQL or archive | Generally restores forward to newer versions | Last successful dump | Lower |
pg_basebackup | Physical cluster copy | Major-version-specific | Last successful base backup | Medium |
| Base backup + WAL archive | Physical backup plus WAL sequence | Major-version-specific | A target covered by retained WAL | Higher |
Choose from the recovery requirement backwards. If losing one backup interval is acceptable, scheduled logical dumps may be enough. If it is not, design and test a continuous-archiving recovery path.
What Is the Sidecar Pattern for Docker Backups?
The sidecar pattern runs a second container alongside your PostgreSQL container, connected via the same Docker network, with the sole job of performing and managing backups. According to Microsoft's cloud architecture patterns documentation, the sidecar pattern "deploys components of an application into a separate process or container to provide isolation and encapsulation."
Here's the architecture:
┌─────────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ PostgreSQL │ │ Backup Sidecar │ │
│ │ Container │◄───│ Container │ │
│ │ │ │ │ │
│ │ Port 5432 │ │ - pg_dump │ │
│ │ (internal) │ │ - gzip │ │
│ │ │ │ - S3 upload │ │
│ │ Volume: │ │ - cron schedule │ │
│ │ pgdata │ │ - retention │ │
│ └──────────────┘ └──────────────────┘ │
│ │
│ No ports exposed to host │
└─────────────────────────────────────────────┘
│ │
▼ ▼
Docker Volume S3 / Object Storage
(live data) (backup files)
Why Not Back Up from the Same Container?
Running the backup tooling in a separate container keeps its dependencies, logs, and lifecycle separate from the live PostgreSQL process. It also lets you apply resource limits and restart policies independently. A sidecar still shares the Docker host, so it can compete with PostgreSQL for CPU, memory, disk, and network unless you actually configure those limits.
A sidecar does not make the backup remote. The dump remains in the same failure domain until the upload completes, which is why the script below treats a failed S3 upload as a failed backup run.
Temps uses the same separation for its logical fallback: it runs pg_dumpall in a one-shot container using the managed service's configured image, joins the shared application network, writes the compressed dump outside the database container, and uploads it to the selected S3 source.
Network-Only Access
The sidecar connects to PostgreSQL over the internal Docker network. No ports are exposed to the host. The connection string uses the service name:
postgresql://backup_user:password@postgres:5432/mydb
Keeping PostgreSQL on an internal Docker network avoids exposing port 5432 solely for the backup job. For a DIY setup, use a dedicated backup role where practical and grant only the access your dump needs; test it against extensions, sequences, and every schema you intend to restore.
The example below uses the application's database role to stay compact. For production, move credentials into Docker secrets or another secret store instead of committing them to Compose.
How Do You Set Up Automated pg_dump with Docker Compose?
Setting up automated PostgreSQL backups requires four pieces: the Compose file defining both containers, a backup script, a scheduler, and retention rules for every place that stores a copy.
Step 1: Docker Compose File
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: myapp
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myapp"]
interval: 10s
timeout: 5s
retries: 5
backup:
build:
context: ./backup
dockerfile: Dockerfile
environment:
PGHOST: postgres
PGPORT: 5432
PGUSER: myapp
PGPASSWORD: ${POSTGRES_PASSWORD}
PGDATABASE: myapp
BACKUP_SCHEDULE: "0 */6 * * *" # Every 6 hours
BACKUP_RETENTION_DAYS: 30
S3_BUCKET: ${S3_BUCKET}
S3_ENDPOINT: ${S3_ENDPOINT}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
volumes:
- backups:/backups
networks:
- backend
depends_on:
postgres:
condition: service_healthy
volumes:
pgdata:
backups:
networks:
backend:
Step 2: Backup Sidecar Dockerfile
FROM postgres:16-alpine
RUN apk add --no-cache \
bash \
aws-cli \
supercronic
COPY backup.sh /usr/local/bin/backup.sh
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/backup.sh /usr/local/bin/entrypoint.sh
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]
The backup image matches the server's PostgreSQL major version. A newer pg_dump can read many older server versions, but an older pg_dump refuses to dump a newer major server. Matching the images avoids that direction-of-compatibility problem.
supercronic runs in the foreground, preserves the container environment for jobs, and sends job output to the container logs. Those properties make failures easier to observe than with a traditional cron daemon configured for a full server.
Step 3: Entrypoint Script
#!/bin/bash
set -euo pipefail
: "${BACKUP_SCHEDULE:?BACKUP_SCHEDULE is required}"
: "${BACKUP_RETENTION_DAYS:?BACKUP_RETENTION_DAYS is required}"
: "${S3_BUCKET:?S3_BUCKET is required}"
case "${BACKUP_RETENTION_DAYS}" in
*[!0-9]*|'')
echo "BACKUP_RETENTION_DAYS must be a positive integer" >&2
exit 1
;;
esac
if [ "${BACKUP_RETENTION_DAYS}" -eq 0 ]; then
echo "BACKUP_RETENTION_DAYS must be greater than zero" >&2
exit 1
fi
# Generate crontab from environment variable
echo "${BACKUP_SCHEDULE} /usr/local/bin/backup.sh" > /etc/crontab
echo "Backup sidecar started"
echo "Schedule: ${BACKUP_SCHEDULE}"
echo "Retention: ${BACKUP_RETENTION_DAYS} days"
echo "Target: ${PGHOST}:${PGPORT}/${PGDATABASE}"
# Run supercronic with the generated crontab
exec supercronic /etc/crontab
Step 4: Backup Script
#!/bin/bash
set -euo pipefail
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"
FILENAME="${PGDATABASE}_${TIMESTAMP}.sql.gz"
FILEPATH="${BACKUP_DIR}/${FILENAME}"
TEMP_FILE="${FILEPATH}.tmp"
cleanup_temp() {
rm -f "${TEMP_FILE}"
}
trap cleanup_temp EXIT
echo "=== Starting backup: ${FILENAME} ==="
# Create a portable plain-SQL dump. Restore it into an empty database.
pg_dump \
--no-owner \
--no-privileges \
--format=plain \
| gzip > "${TEMP_FILE}"
mv "${TEMP_FILE}" "${FILEPATH}"
FILESIZE=$(du -h "${FILEPATH}" | cut -f1)
echo "Backup created: ${FILENAME} (${FILESIZE})"
# Upload to S3-compatible storage. With `set -e`, a failed upload
# stops the run before it can report success or apply local cleanup.
S3_ARGS=()
if [ -n "${S3_ENDPOINT:-}" ]; then
S3_ARGS+=(--endpoint-url "${S3_ENDPOINT}")
fi
aws s3 cp \
"${FILEPATH}" \
"s3://${S3_BUCKET}/pg-backups/${FILENAME}" \
"${S3_ARGS[@]}" \
--quiet
echo "Uploaded to s3://${S3_BUCKET}/pg-backups/${FILENAME}"
# Apply retention policy -- delete local backups older than N days
find "${BACKUP_DIR}" -name "*.sql.gz" -mtime "+${BACKUP_RETENTION_DAYS}" -delete
REMAINING=$(find "${BACKUP_DIR}" -name "*.sql.gz" | wc -l)
echo "Retention applied: ${REMAINING} backups remaining locally"
echo "=== Backup complete ==="
Key Decisions in This Script
--no-owner --no-privileges — These flags avoid replaying ownership changes and grants that refer to roles absent from the target. The trade-off is deliberate: recreate the required roles and privileges separately if your recovery plan needs them.
Restore into an empty database — The example intentionally omits --clean. Restoring a dump over a populated database can destroy or mix data. Create a temporary empty database for tests and use a separately reviewed runbook for a destructive in-place recovery.
Plain format + gzip vs. custom format — Plain SQL is human-readable and restores with psql. Custom or directory format restores with pg_restore and supports selective or parallel restore. Benchmark both with production-shaped data; database size alone does not decide which is better.
S3-compatible storage — The AWS CLI can target a custom S3 endpoint. Providers still differ in region names, addressing style, credentials, object lifecycle support, and other details, so validate an upload, download, and deletion against the provider you actually use.
Remote retention is separate — find only removes old files from the local backup volume. Configure a lifecycle policy on the bucket too, or old remote objects will continue accumulating.
How Do You Test Your Backups?
An untested backup is evidence that a write completed, not evidence that recovery works. Test the same path you would use during an incident: download from remote storage, decompress, restore into an empty target, and verify application-specific data.
Restore to a Test Container
The compact test below expects to run on a dedicated recovery runner with the Docker CLI available and the backup mounted read-only at /backups. It restores the latest local copy. In production, download the object from S3 into that temporary directory first so the test also covers credentials, network access, and the remote object itself. Do not mount the Docker socket into the long-running backup sidecar merely to run this test.
#!/bin/bash
set -euo pipefail
LATEST_BACKUP=""
for candidate in /backups/*.sql.gz; do
[ -e "${candidate}" ] || continue
if [ -z "${LATEST_BACKUP}" ] || [ "${candidate}" -nt "${LATEST_BACKUP}" ]; then
LATEST_BACKUP="${candidate}"
fi
done
: "${LATEST_BACKUP:?No local backup found}"
echo "Testing restore of: ${LATEST_BACKUP}"
RESTORE_CONTAINER="pg_restore_test_$$"
cleanup() {
docker rm -f "${RESTORE_CONTAINER}" >/dev/null 2>&1 || true
}
trap cleanup EXIT
# Start a temporary PostgreSQL container
docker run -d \
--name "${RESTORE_CONTAINER}" \
-e POSTGRES_DB=restore_test \
-e POSTGRES_USER=test \
-e POSTGRES_PASSWORD=test \
postgres:16-alpine
# Wait up to 60 seconds for it to be ready.
for ((attempt = 1; attempt <= 60; attempt++)); do
if docker exec "${RESTORE_CONTAINER}" pg_isready -U test; then
break
fi
if [ "${attempt}" -eq 60 ]; then
echo "Restore target did not become ready" >&2
exit 1
fi
sleep 1
done
# Restore the backup
gunzip -c "${LATEST_BACKUP}" | \
docker exec -i "${RESTORE_CONTAINER}" \
psql -U test -d restore_test --quiet --set ON_ERROR_STOP=on
# Generic smoke check: require at least one restored application table.
TABLE_COUNT=$(docker exec "${RESTORE_CONTAINER}" \
psql -U test -d restore_test -t -c \
"SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'")
echo "Tables restored: ${TABLE_COUNT}"
if [ "${TABLE_COUNT//[[:space:]]/}" -eq 0 ]; then
echo "Restore verification failed: no public tables found" >&2
exit 1
fi
# Add checks for your own critical tables and recent records here.
echo "Restore test complete"
Schedule the Restore Test Separately
Run the restore test from CI or a locked-down recovery runner that can create the temporary PostgreSQL container. Give that runner read-only access to a downloaded backup, not permanent write access to production storage. Schedule it at the interval your recovery policy requires and connect its exit status to alerting or a dead-man monitor so a missing success signal becomes visible.
Measure Your RTO
Recovery Time Objective (RTO) is how long it takes to restore from backup and resume serving traffic. Measure it:
- Time the full restore process — download from S3, decompress, restore, verify
- Add application startup time
- Add DNS propagation if you're switching hosts
Do not borrow an RTO from someone else's database. Object-store throughput, compression, indexes, extensions, CPU, disk, and WAL volume all change the result. Record the measured time and repeat the test after material schema, data-volume, PostgreSQL-version, or infrastructure changes.
What Are the Most Common Backup Mistakes?
These are the failure modes the design above is intended to catch. Review them against your own recovery plan rather than assuming a successful scheduled job covers them.
Mistake 1: Backing Up to the Same Disk
If the backup file sits in a Docker volume on the same host as the database, losing that host can remove both copies. Upload every successful dump to storage outside that host and verify the upload before marking the run successful.
Mistake 2: Never Testing Restores
A scheduled command can run for months while producing an empty, incomplete, or unreachable result. Restore into a clean target using the client tools you will use during recovery, then verify data that matters to the application.
Mistake 3: Missing --no-owner
Without --no-owner, a restore can try to assign objects to roles that do not exist on the target. Decide whether your recovery plan recreates roles or deliberately omits ownership and grants with --no-owner --no-privileges.
Mistake 4: Forgetting PGPASSWORD
An unattended dump cannot answer an interactive password prompt. Supply credentials through a protected mechanism such as Docker secrets or a correctly permissioned .pgpass file, and confirm the scheduled process can authenticate without a terminal.
Mistake 5: Running Backups During Peak Hours
pg_dump does not block ordinary readers or writers, but it holds a consistent snapshot and ACCESS SHARE locks while it runs. Long dumps can delay some schema changes and contribute to retained dead rows. Measure the effect and choose a window that works for the database.
Mistake 6: No Retention Policy
Backups accumulate independently in the local volume and the object store. Set retention in both places based on your recovery requirements, legal obligations, and storage budget. Confirm that deletion works without removing the newest known-good restore point.
Mistake 7: No Alerting on Failure
A cron log is not an alert. Track the time and status of the last successful upload and restore test, then alert when either becomes overdue. A dead-man signal is useful because it catches a scheduler that stopped running entirely.
How Does Temps Handle PostgreSQL Backups Automatically?
Temps includes a built-in backup system for managed databases. From a PostgreSQL service page, you can choose a configured S3-compatible destination and start an on-demand full backup. You can also create schedules with retention and explicit service scope.
For standalone managed PostgreSQL, the current origin/main implementation has two paths (HA clusters use a separate cluster-aware engine):
postgres_walg— uses WAL-G when the managed service image supports it, enables continuous WAL archiving, and creates a base backup in S3postgres_pgdump— falls back topg_dumpall | gzipin a one-shot container when WAL-G is unavailable
What the Current Implementation Does
- Separates the logical fallback from the live database container — the one-shot container uses the service's configured image, joins the application network, and writes a compressed cluster dump before upload
- Enforces retention in two layers — application-side cleanup is the fallback; providers that support object tags and lifecycle configuration can also expire tagged objects while Temps is offline
- Scopes schedules explicitly — a schedule can cover selected services or all current and future services, and can include or exclude the Temps control-plane database
What Temps Automates
After you configure an S3 source, Temps can provide:
- Scheduled backup —
schedule_expression(standard cron syntax) set from the dashboard - WAL-G or
pg_dumpall— WAL-G when the service supports it, with a one-shot logical fallback otherwise - Configurable retention —
retention_periodin days, per schedule - S3-compatible upload — uses the endpoint, region, addressing mode, and credentials configured on the selected source
- Retention fallback — application-side cleanup remains authoritative when a provider does not support the required object tags or lifecycle configuration
- Failure records — tracks overdue schedules and stalled pending backups so the condition is visible to the platform
- Per-schedule database targeting — choose all databases or pick specific services; control whether Temps's own database is included
What You'd Otherwise Build Yourself
Temps handles the backup engine, scheduling, upload, retention, restore orchestration, and backup-state tracking. You still choose and operate the S3-compatible destination, decide the schedule and retention, monitor available storage, and test restores against your own application data. Temps does not automatically certify that a backup is usable.
On-demand backups start from the managed service page. S3 sources and schedules are configured through the backup settings or the Temps CLI, without maintaining a sidecar script or host crontab.
Temps is Apache 2.0 — free to self-host. A managed Temps Cloud add-on covering telemetry retention, offsite backups, and AI credits is coming soon; it hasn't been priced yet.
Frequently Asked Questions
How do you automatically back up PostgreSQL in Docker?
Run a backup container alongside PostgreSQL. It connects over the internal Docker network, runs pg_dump on a schedule, compresses the output, uploads it to S3-compatible storage, and removes expired local copies. Configure remote retention separately and test a restore from the uploaded object. If you prefer a platform workflow, Temps documents the setup here.
How often should you back up PostgreSQL in Docker?
Set the interval from your Recovery Point Objective: the amount of committed data the business can afford to lose. A periodic dump can only recover to its last successful run. If that gap is too large, use continuous WAL archiving and monitor the archive. In both cases, a restore test is separate from backup frequency.
Can you use pg_dump on a running PostgreSQL database?
Yes. pg_dump uses PostgreSQL's MVCC (Multi-Version Concurrency Control) to take a consistent snapshot without locking the database for writes. It holds an ACCESS SHARE lock, which doesn't block normal operations. For very large databases under heavy write load, schedule dumps during low-traffic periods to minimize the impact on autovacuum and replication lag.
What's the difference between pg_dump and pg_dumpall?
pg_dump backs up a single database. pg_dumpall backs up every database in the PostgreSQL cluster plus global objects like roles and tablespaces. In Docker, where you typically run one database per container, pg_dump is usually sufficient. Use pg_dumpall if your container hosts multiple databases or if you need to preserve role definitions across restores.
Should you use pg_dump custom format or plain SQL?
Plain SQL (--format=plain) produces readable output that restores with psql. Custom format (--format=custom) restores with pg_restore and supports selective restore. Parallel restore requires a custom or directory archive and can increase database-server load, so benchmark the format and job count against your recovery environment instead of choosing from a fixed database-size threshold.
Is Temps PostgreSQL backup free?
Yes. Temps is Apache 2.0, and the backup implementation is part of the self-hosted software. You still pay for and operate the server and the S3-compatible storage you choose. Temps Cloud is a separate planned managed add-on and has not shipped or been priced here.
Wrapping Up
Docker volumes are persistent storage, not a recovery plan. The distinction matters when the live data and the only copy share the same failure.
A separate container running pg_dump on a schedule is a reasonable starting point when a periodic logical backup meets your RPO. The recoverable system also includes remote storage, retention, monitoring, credentials, and a tested restore procedure.
What separates teams that recover from data loss and teams that don't isn't the backup tool. It's whether they tested their restores. Automate that too.
If you would rather use the platform workflow, set up a destination, run the first backup, create a schedule, and test a restore in Temps. Whether you build the pipeline yourself or use Temps, do not stop at the successful "backup started" message. Wait for completion and prove the restore while the database is healthy.
Get weekly updates