Self-Host Borgmatic: Automated Encrypted Backups in 15 Minutes
Every self-hoster has the same dirty secret: their backups are a manual tar command they run "when they remember." One dead disk later, that strategy falls apart.
In this guide, you'll set up Borgmatic — a slick wrapper around BorgBackup — to get deduplicated, compressed, encrypted, and scheduled backups running automatically on any VPS or homelab server, all inside Docker.
Why Borgmatic?
Rsync copies files. BorgBackup versions them. Every backup is a full snapshot, but thanks to chunk-level deduplication, only new data costs storage. Add AES-256 client-side encryption and you can safely back up to any untrusted target — another VPS, an offsite box, or even object storage via rclone.
Borgmatic adds the missing ops layer on top of Borg:
- Declarative YAML config instead of long CLI incantations
- Built-in scheduling (no cron babysitting)
- Retention pruning (keep 7 daily, 4 weekly, 6 monthly…)
- Health checks and notification hooks
- Database dump integration (PostgreSQL, MySQL, MongoDB, SQLite)
Prerequisites
- A Linux VPS or server with Docker + Docker Compose plugin installed
- ~10 GB free disk for the backup repository (scale to your data)
- 15 minutes
Step 1: Project Structure
mkdir -p ~/borgmatic/{data,repo,config}
cd ~/borgmatic
Step 2: docker-compose.yml
Create docker-compose.yml:
services:
borgmatic:
image: borgmatic:latest
container_name: borgmatic
restart: unless-stopped
volumes:
# What you want to back up (example: your Docker app data)
- /srv/docker:/data/source:ro
# Backup repository storage
- ./repo:/mnt/borg-repository
# Borgmatic config
- ./config:/etc/borgmatic.d
# Borg cache/state (speeds up runs)
- ./data:/root/.cache/borg
# SSH keys if backing up to a remote host
# - ~/.ssh:/root/.ssh:ro
environment:
- TZ=UTC
- BORG_PASSPHRASE=change-me-to-a-long-random-passphrase
- BACKUP_CRON_EXPRESSION=0 2 * * * # 2 AM daily
Pull and start:
docker compose pull
docker compose up -d
Step 3: Borgmatic Configuration
Create config/config.yaml:
source_directories:
- /data/source
repositories:
- path: /mnt/borg-repository
label: local
encryption_passcommand: sh -c 'echo "$BORG_PASSPHRASE"'
compression: zstd,3
archive_name_format: '{hostname}-{now:%Y-%m-%d-%H%M%S}'
retention:
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
consistency:
checks:
- repository
- archives
check_last: 3
hooks:
before_backup:
- echo "Starting backup $(date)"
after_backup:
- echo "Backup finished $(date)"
on_error:
- echo "BACKUP FAILED — check logs"
# healthchecks:
# ping_url: https://hc-ping.com/your-uuid-here
Step 4: Initialize and Run the First Backup
# One-time repo init
docker exec borgmatic borgmatic init --encryption repokey-blake2
# First manual run
docker exec borgmatic borgmatic prune create check --stats
Expected output ends with something like:
------------------------------------------------------------------------------
Original size Compressed size Deduplicated size
This archive: 4.21 GB 3.88 GB 412.50 MB
All archives: 4.21 GB 3.88 GB 412.50 MB
------------------------------------------------------------------------------
Deduplication ratio on a first run is modest — it gets dramatic on run two, when only changed chunks are stored.
Step 5: Verify It Works
# List archives
docker exec borgmatic borgmatic list
# Restore test — extract latest archive to /tmp/restore
docker exec borgmatic borgmatic extract --archive latest --destination /tmp/restore
# Diff against source
diff -r /srv/docker /tmp/restore/data/source | head
If the diff is empty (or only shows runtime files like logs), your restore path is solid. Always test restores — an untested backup is a hope, not a backup.
Going Offsite (The Real Win)
Local backups protect you from oops, not from fire. Point Borgmatic at a remote Borg server over SSH:
repositories:
- path: ssh://[email protected]/./repo
label: offsite
Or use rclone for S3-compatible targets (MinIO, Wasabi, Hetzner Storage Box) — same config, one extra volume.
Monitoring
Wire the healthchecks.ping_url hook to a free Healthchecks.io (or self-hosted Uptime Kuma) check. You get a dead-man's-switch alert only when backups stop working — no daily noise.
Next Steps
- Database dumps: add
postgresql_databases/mysql_databasesto config.yaml so app DBs are dumped consistently before archiving - Encrypted remote targets: pair this with our MinIO self-hosting guide for an S3-compatible offsite repo
- Full-stack monitoring: combine with Grafana + Uptime Kuma for visibility across your whole homelab
- Security: back up Vaultwarden or Authentik data directories the same way
Fifteen minutes of setup buys you versioned, encrypted, automatic backups that will outlive whatever disk fails next. Your future self — the one doing a calm restore at 2 AM instead of a panicked rebuild — says thanks.