Skip to Content

Self-Host Borgmatic: Automated Encrypted Backups in 15 Minutes

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_databases to 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.

Self-Host Nextcloud: Your Own Private Google Drive in 20 Minutes
Deploy your own private cloud storage with Docker Compose — complete step-by-step guide