All apps · 0 apps
database-backup
Docker app from GreiteTurtle's Repository
Overview
Lightweight Go-powered container to automate PostgreSQL, MariaDB/MySQL, and MongoDB backups on a schedule.
Features:
- Simple YAML configuration with strict validation and a built-in validate command
- Dynamic client install at startup: only the tools your config needs are installed
- Flexible cron scheduling per backup job, with graceful shutdown for in-flight backups
- Streaming gzip compression, atomic file writes, timestamped dumps
- Optional at-rest encryption: gpg-compatible OpenPGP or age (passphrase or public key)
- Configurable per-job retention; old backups are only rotated after a successful run
- Runs unprivileged (PUID/PGID, default 99:100 on Unraid) after client installation; secrets never appear in process arguments
- Built-in healthcheck using native database drivers (no heavy CLI downloads)
- Multiple databases (different types and hosts) handled by a single container
- Multi-architecture (amd64, arm64) and auto-rebuilt on base image security updates
Setup:
- Copy backups.yml.example from the project repository to your appdata directory and rename it to backups.yml
- Edit backups.yml with your databases (chmod 600, it contains credentials)
- Map the file to /config/backups.yml and a host folder to /backups
Upgrading from v1: run the built-in migrate command to convert your old backups.conf to YAML. See https://github.com/Greite/database-backup/blob/main/docs/MIGRATION-V2.md
Readme
View on GitHub
Lightweight Docker image based on Debian Slim to automate PostgreSQL, MariaDB/MySQL, MongoDB and SQLite database backups with a built-in Go scheduler.
Features
- PostgreSQL (any client version available in the PGDG repository; default 18), MariaDB/MySQL, MongoDB and SQLite (file-based, e.g. Vaultwarden) 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 an unprivileged uid/gid after installing clients (
PUID/PGID, default 1000) - 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)
Quick Start
Using the pre-built image from GitHub Container Registry
docker pull ghcr.io/greite/database-backup:latest
Example 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-network
Available Tags
latest— Latest stable versionv2.0.0— Tagged release version (semantic versioning:v2.0.0,2.0,2)
Unraid (Community Applications)
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.
Project Structure
.
├── 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…)
Configuration
Config File (backups.yml)
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), and make it owned by the uid the container runs as (PUID, default 1000) so it stays readable after the privilege drop.
See backups.yml.example for a ready-to-use template.
Schema
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, mongodb, or sqlite |
host |
Yes* | — | Database hostname or IP |
port |
No | 5432 / 3306 / 27017 | Connection port |
database |
Yes* | — | Database name to back up |
path |
Yes* | — | Absolute path of the database file inside the container; SQLite only |
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). SQLite jobs take only path: host, database and credentials do not apply.
SQLite jobs
SQLite databases are files, so the application's data directory must be mounted into the backup container, read-only is enough:
services:
db-backup:
volumes:
- /srv/vaultwarden/data:/sources/vaultwarden:ro
jobs:
- name: vaultwarden
type: sqlite
path: /sources/vaultwarden/db.sqlite3
schedule: "0 3 * * *"
The dump is sqlite3 -readonly <path> .dump, a consistent snapshot taken inside a single read transaction, so it is safe while the application keeps writing. The file must be readable by PUID. For a database in WAL mode (Vaultwarden's default) the -wal and -shm side files must either exist, which they do while the application is running, or be creatable by PUID in the same directory; otherwise the job fails (sqlite3 reports unable to open database file or attempt to write a readonly database). A failed job never leaves a truncated backup behind.
Encryption block
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_passphrase
age (public-key recipients) — the container never holds the private key:
encryption:
method: age
recipients:
- age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
The 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.
Docker secrets (password_file)
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
Usage
Volumes
| Mount point | Purpose |
|---|---|
/config/backups.yml |
YAML configuration file (required) |
/backups |
Directory where backup files are written |
any, e.g. /sources/<app> |
Data directory of an application whose SQLite file is backed up (:ro is enough) |
Environment variables
| Variable | Default | Description |
|---|---|---|
TZ |
UTC |
Timezone for log lines and backup file names (e.g. Europe/Paris) |
PUID |
1000 |
UID the scheduler and dumps run as; /backups is chowned to it at startup (linuxserver.io-style). Unraid: 99. backups.yml must be readable by this uid (it is not chowned) |
PGID |
1000 |
GID the scheduler and dumps run as. Unraid: 100 |
BACKUP_ENCRYPTION_PASSPHRASE |
- | v1-compatible GPG passphrase, used when no encryption: block is present |
BACKUP_ENCRYPTION_PASSPHRASE_FILE |
- | Path to a file containing the passphrase above |
Subcommands
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 one named job, or all jobs when --job is omitted (use with docker exec) |
list |
Print the configured jobs (name, type, target, schedule, retention) |
healthcheck |
Check connectivity for all configured databases and exit |
migrate [path] |
Convert a v1 pipe-delimited file to YAML and print to stdout |
Docker Compose (recommended)
- 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-backup
Docker Run
docker run -d \
--name db-backup \
-v $(pwd)/backups:/backups \
-v $(pwd)/backups.yml:/config/backups.yml:ro \
ghcr.io/greite/database-backup:latest
Validate before deploying
docker run --rm \
-v $(pwd)/backups.yml:/config/backups.yml:ro \
ghcr.io/greite/database-backup:latest validate
Trigger a manual backup
docker exec db-backup dbbackup list # see the job names
docker exec db-backup dbbackup backup --job myapp # one job
docker exec db-backup dbbackup backup # all jobs, sequentially
Migrating from v1
v2 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.yml
Review the output, then mount it at /config/backups.yml.
See docs/MIGRATION-V2.md for the full field mapping and list of behavior changes.
Managing Backups
Backup file structure
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
│ └── ...
└── sqlite/
└── vaultwarden/
├── vaultwarden_20250131_030000.sql.gz
└── ...
Note: MongoDB backups use
.tar.gzformat (compressed BSON archive), while PostgreSQL, MariaDB and SQLite use.sql.gz(compressed SQL dump). Encrypted backups get an additional.gpgor.agesuffix.
Restore a backup
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_db
MariaDB:
gunzip -c backups/mariadb/wordpress/wordpress_20250131_030000.sql.gz | \
mysql -h localhost -u root -p wordpress
MongoDB:
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_restore
SQLite:
# Restores into a new file; stop the application before swapping it in.
gunzip -c backups/sqlite/vaultwarden/vaultwarden_20250131_030000.sql.gz | \
sqlite3 db.sqlite3
Security
Password handling
All 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.
TLS connections
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.
Best practices
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
/backupsOff-site backups: Regularly sync backups to external storage (S3, NAS, etc.)
Monitoring
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 healthcheck
Dynamic client installation
No 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 - SQLite: installs
sqlite3
Internet access is required on first startup. First startup may take an extra 30–90 seconds depending on which clients need to be installed.
CI/CD with GitHub Actions
Release Build (.github/workflows/docker-build.yml)
Triggered on version tags (v*.*.*) or manual dispatch. Builds and pushes the Docker image to GitHub Container Registry with semantic versioning tags.
Base Image Update Check (.github/workflows/base-image-check.yml)
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.
Creating a release
git tag -a v2.0.0 -m "Release version 2.0.0"
git push origin v2.0.0
This 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
License
This project is licensed under the MIT License — see the LICENSE file for details.
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/greite/database-backup:latestRuntime arguments
- Network
bridge- Shell
bash- Privileged
- false
Template configuration
Host path where database dumps are written. Files are organized as type/job-name/job-name_timestamp.sql.gz (or .tar.gz for MongoDB), plus a .gpg or .age suffix when encryption is enabled.
- Target
- /backups
- Default
- /mnt/user/appdata/database-backup/backups
- Value
- /mnt/user/appdata/database-backup/backups
Path to the backups.yml file on the host. Get the example from https://github.com/Greite/database-backup/blob/main/backups.yml.example and edit it before starting the container. Keep it chmod 600 and owned by PUID (99 by default) so the container can read it. Upgrading from v1? Convert your backups.conf with the built-in migrate command (see the migration guide linked in the Overview).
- Target
- /config/backups.yml
- Default
- /mnt/user/appdata/database-backup/backups.yml
- Value
- /mnt/user/appdata/database-backup/backups.yml
Optional. Enables gpg-compatible at-rest encryption of every backup (decrypt with gpg -d). Leave empty to disable, or use the encryption block in backups.yml for more options (age, key files).
- Target
- BACKUP_ENCRYPTION_PASSPHRASE
UID the scheduler and dumps run as. The Backups Storage path is chowned to it at startup. 99 = nobody (Unraid default). Requires v2.1.0 or later.
- Default
- 99
- Value
- 99
GID the scheduler and dumps run as. 100 = users (Unraid default). Requires v2.1.0 or later.
- Default
- 100
- Value
- 100