Lightweight Docker image based on Debian Slim to automate PostgreSQL, MariaDB/MySQL, and MongoDB database backups with a built-in Go scheduler.
- PostgreSQL (any client version available in the PGDG repository; default 18), MariaDB/MySQL, and MongoDB support
- Dynamic client installation at startup (only required tools are installed based on config)
- Lightweight image (~126 MB): no database clients pre-installed
- YAML configuration (
backups.yml) with full validation at startup - In-process scheduler — no external cron daemon
- Streaming gzip compression for all dump formats
- Optional at-rest encryption: GPG (symmetric OpenPGP) or age (passphrase or public-key recipients)
- Optional TLS for database connections
- Credentials never exposed on process command lines
- Non-root runtime: container drops to uid 1000 after installing clients
- Built-in healthcheck using native database drivers (no
mongoshdownload) - Migration command to convert v1
backups.confto YAML - Centralized logging to container stdout
- Automated build via GitHub Actions
- Multi-architecture images (amd64, arm64)
- Automatic rebuild when the base image (
debian:trixie-slim) is updated (4 times daily)
docker pull ghcr.io/greite/database-backup:latestExample compose.yml using the pre-built image:
services:
db-backup:
image: ghcr.io/greite/database-backup:latest
container_name: db-backup
restart: unless-stopped
volumes:
- ./backups:/backups
- ./backups.yml:/config/backups.yml:ro
networks:
- db-networklatest— Latest stable versionv2.0.0— Tagged release version (semantic versioning:v2.0.0,2.0,2)
An Unraid template for this image is maintained in the Greite/unraid-templates repository and is published on the Community Applications store.
Install via Community Applications:
- Open the Apps tab in your Unraid web UI
- Search for
database-backup - Click Install and adjust the paths:
- Backups Storage — host path that will receive the dumps (default
/mnt/user/appdata/database-backup/backups) - Configuration File — host path to your
backups.yml(default/mnt/user/appdata/database-backup/backups.yml); copybackups.yml.examplethere first and edit it - Timezone — IANA timezone name (default
Europe/Paris)
- Backups Storage — host path that will receive the dumps (default
- Click Apply
Manual install (without Community Applications):
In the Unraid web UI go to Docker → Add Container, set the Template URL to:
https://raw.githubusercontent.com/Greite/unraid-templates/main/database-backup/database-backup.xml
Then load the template and follow the same steps as above.
.
├── Dockerfile
├── compose.yml # Example with test databases
├── backups.yml.example # Configuration template
├── docs/
│ └── MIGRATION-V2.md # Migration guide from v1
├── .github/
│ └── workflows/
│ ├── docker-build.yml # Build workflow on version tags
│ ├── go-ci.yml # Go lint, unit, and integration tests
│ └── base-image-check.yml # Scheduled rebuild on base image update
├── cmd/
│ └── dbbackup/ # Main binary
└── internal/ # Internal packages (config, scheduler, crypto…)
Mount the file at /config/backups.yml inside the container. Use chmod 600 on the host because the file contains credentials (or use password_file everywhere).
See backups.yml.example for a ready-to-use template.
Top-level fields:
| Field | Required | Default | Description |
|---|---|---|---|
defaults.retention_days |
No | 7 |
Retention days applied to all jobs unless overridden |
defaults.tls |
No | false |
TLS flag applied to all jobs unless overridden |
shutdown_grace |
No | 5m |
Grace period for in-flight backups on container stop. Must be a duration string ("5m", "300s", "1h") — bare integers are rejected |
encryption |
No | — | At-rest encryption block (see below) |
jobs |
Yes | — | List of backup jobs |
Per-job fields:
| Field | Required | Default | Description |
|---|---|---|---|
name |
Yes | — | Unique job identifier (used in log output and directory paths) |
type |
Yes | — | postgres, mariadb, mysql, or mongodb |
host |
Yes | — | Database hostname or IP |
port |
No | 5432 / 3306 / 27017 | Connection port |
database |
Yes | — | Database name to back up |
user |
No* | — | Database user |
password |
No* | — | Inline password (any character, including |) |
password_file |
No* | — | Path to a file containing the password (e.g. a Docker secret) |
schedule |
Yes | — | Cron expression ("0 2 * * *") or shorthand ("@daily") |
retention_days |
No | from defaults or 7 |
Number of days to keep backups |
pg_version |
No | 18 |
PostgreSQL client version (any version available in the PGDG repository, default 18); PostgreSQL only |
tls |
No | from defaults or false |
true to encrypt the database connection |
*user and password / password_file are required for PostgreSQL and MariaDB. For MongoDB they are optional (omit for unauthenticated dev/test setups).
GPG (symmetric OpenPGP) — decrypt with gpg -d file.gpg:
encryption:
method: gpg
passphrase_file: /run/secrets/backup_passphrase
# or: passphrase: my_passphrase (visible in docker inspect — avoid in production)age (passphrase) — decrypt with age -d:
encryption:
method: age
passphrase_file: /run/secrets/backup_passphraseage (public-key recipients) — the container never holds the private key:
encryption:
method: age
recipients:
- age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8pThe v1 environment variables BACKUP_ENCRYPTION_PASSPHRASE and BACKUP_ENCRYPTION_PASSPHRASE_FILE remain supported and produce the same GPG-compatible files when no encryption: block is present.
services:
db-backup:
image: ghcr.io/greite/database-backup:latest
secrets:
- db_password
# In backups.yml: password_file: /run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt| Mount point | Purpose |
|---|---|
/config/backups.yml |
YAML configuration file (required) |
/backups |
Directory where backup files are written |
The container binary (dbbackup) supports the following subcommands. When no subcommand is given, run is the default.
| Subcommand | Description |
|---|---|
run |
Load config and start the built-in scheduler (default) |
validate |
Parse and validate the config, then exit; useful in CI |
backup --job <name> |
Run a single named job immediately (use with docker exec) |
healthcheck |
Check connectivity for all configured databases and exit |
migrate [path] |
Convert a v1 pipe-delimited file to YAML and print to stdout |
- Copy and edit the example config:
cp backups.yml.example backups.yml
chmod 600 backups.yml
# Edit backups.yml with your settings- Start the services:
docker compose up -d- Check the logs:
docker compose logs -f db-backupdocker run -d \
--name db-backup \
-v $(pwd)/backups:/backups \
-v $(pwd)/backups.yml:/config/backups.yml:ro \
ghcr.io/greite/database-backup:latestdocker run --rm \
-v $(pwd)/backups.yml:/config/backups.yml:ro \
ghcr.io/greite/database-backup:latest validatedocker exec db-backup dbbackup backup --job myappv2 replaces the pipe-delimited backups.conf with YAML (backups.yml). An automatic conversion command is provided:
docker run --rm -v /path/to/appdata:/config \
ghcr.io/greite/database-backup:2 migrate /config/backups.conf > backups.ymlReview the output, then mount it at /config/backups.yml.
See docs/MIGRATION-V2.md for the full field mapping and list of behavior changes.
Backups are organized by type and job name (<type>/<job-name>/). For configs
migrated from v1 the job name equals the database name, so the paths look like:
backups/
├── postgres/
│ └── myapp_db/
│ ├── myapp_db_20250131_020000.sql.gz
│ ├── myapp_db_20250130_020000.sql.gz
│ └── ...
├── mariadb/
│ └── wordpress/
│ ├── wordpress_20250131_030000.sql.gz
│ └── ...
└── mongodb/
└── events/
├── events_20250131_000000.tar.gz
└── ...
Note: MongoDB backups use
.tar.gzformat (compressed BSON archive), while PostgreSQL and MariaDB use.sql.gz(compressed SQL dump). Encrypted backups get an additional.gpgor.agesuffix.
If at-rest encryption is enabled, first decrypt the file:
# GPG gpg --batch --passphrase-file /path/to/passphrase \ --decrypt myapp_db_20250131_020000.sql.gz.gpg > myapp_db_20250131_020000.sql.gz # age age --decrypt -i /path/to/key myapp_db_20250131_020000.sql.gz.age > myapp_db_20250131_020000.sql.gz
PostgreSQL:
gunzip -c backups/postgres/myapp_db/myapp_db_20250131_020000.sql.gz | \
psql -h localhost -U postgres -d myapp_dbMariaDB:
gunzip -c backups/mariadb/wordpress/wordpress_20250131_030000.sql.gz | \
mysql -h localhost -u root -p wordpressMongoDB:
mkdir -p /tmp/mongo_restore
tar -xzf backups/mongodb/events/events_20250131_000000.tar.gz -C /tmp/mongo_restore
mongorestore --uri="mongodb://admin:password@localhost:27017/events?authSource=admin" \
--gzip --drop /tmp/mongo_restore/events
rm -rf /tmp/mongo_restoreAll passwords are written at startup to per-job files (root-only, mode 600) and are never passed on command lines — argv is world-readable through /proc/*/cmdline, so nothing sensitive ever appears in ps aux or docker top.
Prefer password_file over inline password in production; Docker secrets mount the file at /run/secrets/<name> automatically.
Set tls: true on a job (or under defaults) to encrypt the connection: PGSSLMODE=require for PostgreSQL, --ssl for MariaDB/MySQL, --ssl/--tls for MongoDB. Recommended when the database is not on the same private Docker network.
-
File permissions:
chmod 600 backups.ymlon the host (the container warns at startup if the mounted file is group/other-readable) -
Dedicated backup users: Create users with minimal privileges:
PostgreSQL:
CREATE USER backup_user WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE myapp_db TO backup_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO backup_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO backup_user;MariaDB:
CREATE USER 'backup_user'@'%' IDENTIFIED BY 'secure_password';
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON myapp_db.* TO 'backup_user'@'%';
FLUSH PRIVILEGES;MongoDB:
use admin
db.createUser({
user: "backup_user",
pwd: "secure_password",
roles: [
{ role: "backup", db: "admin" },
{ role: "read", db: "myapp" }
]
})-
Backup storage: Consider mounting an encrypted volume for
/backups -
Off-site backups: Regularly sync backups to external storage (S3, NAS, etc.)
The container includes a healthcheck that verifies all configured databases are reachable using native drivers. It runs every 5 minutes with a 30-second timeout.
# View healthcheck status
docker inspect --format='{{.State.Health.Status}}' db-backup
# View last healthcheck details
docker inspect --format='{{json .State.Health}}' db-backup | jq
# Run the healthcheck manually
docker exec db-backup dbbackup healthcheckNo database client is pre-installed in the Docker image. At startup the container parses the configuration and installs only the required tools:
- PostgreSQL: installs the specific configured version (
pg_version, any version available in the PGDG repository, default 18) - MariaDB/MySQL: installs
mariadb-client - MongoDB: installs
mongodumpandmongorestore
Internet access is required on first startup. First startup may take an extra 30–90 seconds depending on which clients need to be installed.
Triggered on version tags (v*.*.*) or manual dispatch. Builds and pushes the Docker image to GitHub Container Registry with semantic versioning tags.
Runs automatically 4 times daily (at 00:00, 06:00, 12:00, and 18:00 UTC) to check if the debian:trixie-slim base image has been updated. If a new version is detected, it automatically rebuilds and pushes the image.
git tag -a v2.0.0 -m "Release version 2.0.0"
git push origin v2.0.0This automatically creates the following Docker tags:
ghcr.io/greite/database-backup:v2.0.0ghcr.io/greite/database-backup:2.0ghcr.io/greite/database-backup:2ghcr.io/greite/database-backup:latest
This project is licensed under the MIT License — see the LICENSE file for details.
