Container image to Backup, Restore and Migrate PostgreSQL database to Local, AWS S3 or SSH, Azure.
100K+
PG-BKUP backs up, restores and migrates PostgreSQL databases. It streams pg_dump output through compression and encryption straight into the storage: no temporary dump file, constant memory, whatever the database size.
Documentation: https://jkaninda.github.io/pg-bkupโ ยท MySQL: mysql-bkupโ
pg_dump | gzip/zstd | GPG/age | upload, and back for restores. Nothing is written to disk.psql -v ON_ERROR_STOP=1 --single-transaction (custom archives too, through pg_restore -f -): a failing or corrupted backup changes nothing, and a dump from another server is not stopped by its roles or its pg_dump version.pg_dump that is not older than the server (servers 9.2 and newer). No more "server version mismatch".BACKUP_RETENTION_DAYS) or count (BACKUP_RETENTION_COUNT); only backups recorded in the index are ever deleted.backup, restore, migrate, verify, list, delete, prune, reconcile, config, health.--cron-expression daemon with a Docker HEALTHCHECK and optional Prometheus metrics..sql) or custom (PG_DUMP_FORMAT=custom, .dump); schema-only, data-only, selected or excluded tables, all databases one file each, or the whole instance with pg_dumpall.docker run --rm --network your_network \
-v $PWD/backup:/backup \
-e DB_HOST=postgres -e DB_USER=postgres -e DB_PASSWORD=password \
jkaninda/pg-bkup backup -d database_name
The last line of a successful backup always names the artifact:
level=INFO msg="Backup completed" database=database_name file=database_name_20261027_031500.sql.gz size=12.3MiB duration=2.1s storage=local location=/backup
Restore it (the target database must exist):
docker run --rm --network your_network \
-v $PWD/backup:/backup \
-e DB_HOST=postgres -e DB_USER=postgres -e DB_PASSWORD=password \
jkaninda/pg-bkup restore -d database_name -f database_name_20261027_031500.sql.gz
restore accepts .sql, .sql.gz, .sql.zst, .dump (custom format) and any of them encrypted (.gpg, .age). The format is detected from the content, not the name.
A connection URL works too: DB_URL=postgres://user:p%40ss@host:5432/db?sslmode=require or the 1.x JDBC form jdbc:postgresql://host:5432/db?user=user&password=pass.
| Command | File name |
|---|---|
backup -d shop | shop_20261027_031500.sql.gz |
backup -d shop --schema-only | shop_schema_20261027_031500.sql.gz |
backup -d shop --data-only | shop_data_20261027_031500.sql.gz |
backup -d shop -t users -t orders (or --tables users,orders) | shop_tables_2_20261027_031500.sql.gz |
backup -d shop --exclude-table audit_log | shop_20261027_031500.sql.gz |
backup -d shop --custom-name nightly | nightly.sql.gz (overwritten each run) |
PG_DUMP_FORMAT=custom backup -d shop | shop_20261027_031500.dump.gz |
backup --all-databases --exclude-db _aiven,defaultdb | one file per database |
backup --all-in-one --no-role-passwords | all_databases_20261027_031500.sql.gz (pg_dumpall: roles, tablespaces, every database) |
--all-databases skips templates and the postgres database; databases are listed from DB_AUTH_DATABASE (default postgres).
An --all-in-one file is restored with restore -f all_databases_....sql.gz (no -d): it is replayed without a single transaction and, by default, without ON_ERROR_STOP, because pg_dumpall output always contains statements such as CREATE ROLE postgres that fail on an existing server (RESTORE_ON_ERROR_STOP).
The custom format is dumped with pg_dump -Fc -Z0 and compressed by the pipeline (parallel gzip or zstd), so COMPRESSION applies to both formats and every artifact is decompressed the same way. Custom-format archives are turned into SQL by pg_restore --no-owner -f - and replayed by the same psql as plain dumps (RESTORE_NO_OWNER=false keeps ownership).
migrate pipes pg_dump into psql on the target (TARGET_DB_*), with no intermediate file:
docker run --rm --network your_network \
-e DB_HOST=old -e DB_USER=postgres -e DB_PASSWORD=pw -e DB_NAME=shop \
-e TARGET_DB_HOST=new -e TARGET_DB_USER=postgres -e TARGET_DB_PASSWORD=pw -e TARGET_DB_NAME=shop \
jkaninda/pg-bkup migrate
migrate --all-databases (-a) copies every database into the same name, creating missing ones.migrate --entire-instance (-I) copies roles, tablespaces and every database with pg_dumpall (add --no-role-passwords to keep the target's passwords).TARGET_DB_URL, TARGET_DB_SSL_MODE, TARGET_DB_SSL_CA/CERT/KEY and TARGET_DB_AUTH_DATABASE are honoured.# S3 / MinIO
-e AWS_S3_ENDPOINT=https://s3.amazonaws.com -e AWS_S3_BUCKET_NAME=backups -e AWS_REGION=us-east-1 \
-e AWS_ACCESS_KEY=... -e AWS_SECRET_KEY=... \
jkaninda/pg-bkup backup --storage s3 -d shop --path /prod/shop
# MinIO: add AWS_DISABLE_SSL=true (plain http) and AWS_FORCE_PATH_STYLE=true.
# Without keys, the AWS default credential chain (IAM role, IRSA, profile) is used.
# SFTP (host keys are verified: mount a known_hosts file)
-e SFTP_HOST=backup.example.com -e SFTP_USER=backup -e SFTP_IDENTITY_FILE=/config/id_ed25519 \
-e SFTP_KNOWN_HOSTS=/config/known_hosts \
jkaninda/pg-bkup backup --storage sftp -d shop --path /home/backup/shop
# FTP, Azure Blob, Google Cloud Storage
jkaninda/pg-bkup backup --storage ftp -d shop --path /backups # FTP_HOST, FTP_USER, FTP_PASSWORD [FTP_TLS]
jkaninda/pg-bkup backup --storage azure -d shop --path backups # AZURE_STORAGE_ACCOUNT_NAME, _KEY (or _SAS_TOKEN), _CONTAINER_NAME
jkaninda/pg-bkup backup --storage gcs -d shop --path backups # GCS_BUCKET, GCS_CREDENTIALS_FILE
restore takes the same --storage and --path plus the bare file name: restore --storage s3 --path /prod/shop -d shop -f shop_20261027_031500.sql.gz.gpg.
| Mode | Backup | Restore |
|---|---|---|
| GPG passphrase | GPG_PASSPHRASE | GPG_PASSPHRASE |
| GPG public key | GPG_PUBLIC_KEY (path or inline), or /config/gnupg/public_key.{asc,gpg} | GPG_PRIVATE_KEY (+ GPG_PRIVATE_KEY_PASSPHRASE, defaults to GPG_PASSPHRASE) |
| age recipients | AGE_RECIPIENTS (age1..., ssh-ed25519 ...) or AGE_RECIPIENTS_FILE | AGE_IDENTITY / AGE_IDENTITY_FILE (default /config/age/identity.txt) |
| age passphrase | AGE_PASSPHRASE | AGE_PASSPHRASE |
ENCRYPTION=auto (default) picks from the keys that are set; GPG wins when both are set. Encryption fails closed: a configured but unusable key aborts the backup. Encrypted files end in .gpg or .age.
Each storage directory holds an index (BACKUP_INDEX_NAME, default pg-bkup-metadata.json, placeholders {tool} {engine} {name} {db}) and a <artifact>.meta.json sidecar per backup, with size, sha256, server version and encryption.
BACKUP_RETENTION_DAYS=7 and/or BACKUP_RETENTION_COUNT=10 prune after each backup; the newest backup is always kept.BACKUP_RETENTION_ADOPT_LEGACY=true; BACKUP_INDEX=false falls back to name-pattern retention.list, verify -f FILE, delete -f FILE, prune [--dry-run] and reconcile work from the index.docker run -d --name pg-bkup --network your_network -v $PWD/backup:/backup \
-e DB_HOST=postgres -e DB_USER=postgres -e DB_PASSWORD=password -e BACKUP_RETENTION_DAYS=7 \
jkaninda/pg-bkup backup -d database_name --cron-expression "@daily"
Cron expressions take 5 or 6 fields, @daily/@hourly/... or @every 6h. BACKUP_RUN_ON_START=true runs one backup immediately. A failed run is logged and notified and the daemon keeps running. docker inspect shows the health status (pg-bkup health); METRICS_ADDR=:9090 serves Prometheus metrics.
On Kubernetes, prefer a CronJob running a one-shot backup (see the docsโ ).
# /config/config.yaml (or --config / BACKUP_CONFIG_FILE)
cronExpression: "@daily" # optional
databases:
- host: lldap-db
name: lldap
user: lldap
password: ${LLDAP_DB_PASSWORD} # ${VAR} and ${VAR:-default} are expanded
path: /s3-path/lldap
- name: keycloak # host/user/password from DB_HOST_KEYCLOAK or KEYCLOAK_DB_HOST, ... then DB_HOST
path: /s3-path/keycloak
sslMode: require
For each entry: the YAML field wins, then <KEY>_<NAME> / <NAME>_<KEY> env vars (e.g. DB_PASSWORD_KEYCLOAK, KEYCLOAK_DB_PASSWORD), then the global variable.
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | failure (dump, restore, ...) |
| 2 | configuration or usage error |
| 3 | connection or authentication error |
| 4 | storage error |
| 5 | encryption error |
--result-file PATH writes the result as JSON; LOG_FORMAT=json makes the logs machine-readable.
Copyright (c) [2024] [Jonas Kaninda]
Content type
Image
Digest
sha256:af3195a72โฆ
Size
24.3 MB
Last updated
5 days ago
docker pull jkaninda/pg-bkup