Sign inSign up

diarmuidk/docker-borg-client

By diarmuidk

•Updated 13 days ago

A minimal, generic Docker container for running BorgBackup backups.

Image
Developer tools
Databases & storage
0

3.6K

diarmuidk/docker-borg-client repository overview

⁠Docker Borg Client

Docker Image Version Docker Image Size Docker Pulls Docker Stars CI Tests GitHub release Licence

A minimal, generic Docker container for running BorgBackup⁠ (source⁠) backups to any remote SSH-accessible Borg repository. Designed for TrueNAS but works anywhere Docker runs.

Built on Borg 1.4.4 (pinned to the Alpine borgbackup package; see Dockerfile⁠).

⁠Overview

Why Docker Borg Client?

If you're running a home server, NAS, or any system with important data, you need reliable, automated backups. This container solves my backup problem of TrueNAS to a remote Borg:

  • Set it and forget it - Configure once, runs forever. Automated backups on your schedule with smart retention policies that keep recent backups while pruning old ones.
  • Production Upstream technology - Built on BorgBackup⁠, used by thousands for petabytes of data. Provides deduplication, compression, and encryption that can reduce backup sizes by 95%+.
  • Cost-effective - Works with any SSH-accessible storage: a Raspberry Pi at a friend's house, cloud hosting provider, or any VPS. No vendor lock-in.
  • TrueNAS optimized - Built for the TrueNAS ecosystem - but in theory should run on an docker enginer.
  • Data Safety First - Built, tested, and refined on real production data. Features automatic integrity checking, checkpoint-based resumption for interrupted backups, and comprehensive restore verification tools. Your backups are only as good as your ability to restore them.
  • Security by Design - Client-side encryption before data leaves your server. Even if your backup destination is compromised, your data remains encrypted and safe. Zero-knowledge architecture means only you have the keys.

Perfect for:

  • Home lab enthusiasts backing up Docker volumes, databases, and configuration
  • TrueNAS users wanting automated off-site backups of their datasets
  • Small businesses needing GDPR-compliant encrypted backups
  • Anyone who learned the hard way that RAID is not a backup

⁠Table of Contents

⁠Features

  • 🔒 Secure: Client-side encryption with SSH key authentication
  • 🐧 Minimal: Alpine Linux + Borg + SSH client only
  • 🔧 Generic: Works with any Borg-over-SSH target (Hetzner, rsync.net, self-hosted)
  • 📅 Automated: Configurable cron-based scheduled backups
  • ♻️ Smart retention: Automatic pruning with configurable policies
  • 🚑 Proven recovery: Scheduled restore drills⁠ that actually restore files and compare them to the source
  • 🛡️ Fail-safes: Startup recovery-readiness checks⁠, refusal to restore over live data, drills that never break a running backup's lock
  • 🏗️ Multi-arch: Supports amd64 and arm64

⁠Quick Start

⁠Prerequisites
  1. A remote Borg repository accessible via SSH (see Provider Guides⁠)
  2. SSH key pair for authentication
  3. Docker and Docker Compose installed (or TrueNAS SCALE)
⁠TrueNAS SCALE Setup

TrueNAS SCALE users can deploy this container using the Custom App feature:

  1. Prepare SSH Keys:

    • Create a directory for your SSH keys:
      mkdir -p /mnt/pool/borg-backup/ssh
      ssh-keygen -t ed25519 -f /mnt/pool/borg-backup/ssh/key -N ""
      chmod 700 /mnt/pool/borg-backup/ssh
      chmod 600 /mnt/pool/borg-backup/ssh/key
      
    • Add the public key to your remote backup server:
      cat /mnt/pool/borg-backup/ssh/key.pub
      
  2. Deploy Custom App:

    • Navigate to Apps → Discover Apps → Custom App
    • Click Install on Custom App
    • Configure the following:

    Application Name: borg-backup

    Image Configuration:

    • Image Repository: diarmuidk/docker-borg-client
    • Image Tag: latest
    • Image Pull Policy: Always

    Container User and Group (under Advanced Settings):

    • User ID: 0

    • Group ID: 0

    Note: This container runs as root (UID/GID 0:0) to ensure reliable access to all backup paths. Backup containers require broad filesystem access by design. Privileged mode is not required.

    Environment Variables (Required - add all of these):

    BORG_REPO=ssh://[email protected]:22/~/backups
    BORG_PASSPHRASE=your-strong-passphrase-here
    BACKUP_PATHS=/data/dataset1:/data/dataset2
    CRON_SCHEDULE=0 2 * * 0
    PRUNE_KEEP_DAILY=7
    PRUNE_KEEP_WEEKLY=4
    PRUNE_KEEP_MONTHLY=6
    AUTO_INIT=true
    

    Note: Set timezone using TrueNAS's built-in Timezone dropdown (under Advanced Settings), not as an environment variable.

    Optional - Time Window Configuration (for large initial backups):

    BACKUP_WINDOW_START=01:00
    BACKUP_WINDOW_END=07:00
    BACKUP_RATE_LIMIT_IN_WINDOW=-1
    BACKUP_RATE_LIMIT_OUT_WINDOW=0
    

    This configuration runs backups only during 1am-7am at full speed, perfect for large initial backups on limited connections.

    Storage:

    • Add Host Path Volume for SSH keys:

      • Host Path: /mnt/pool/borg-backup/ssh
      • Mount Path: /ssh
      • Read Only: ✅ Enable
    • Add Host Path Volume for each dataset to backup:

      • Host Path: /mnt/pool/your-dataset
      • Mount Path: /data/dataset1
      • Read Only: ✅ Enable
    • Add ixVolume for Borg cache:

      • Mount Path: /borg/cache
    • Add ixVolume for Borg config:

      • Mount Path: /borg/config

    Restart Policy: Unless Stopped

  3. Initialize Repository:

    Option A - Automatic (Recommended):

    • Add AUTO_INIT=true to environment variables
    • Start the app - repository will be automatically initialized on first run
    • Check logs to see initialization message and backup the credentials
    • Navigate to Apps → Installed → borg-backup → Shell
    • Run: cat /borg/config/repo-key.txt and save to password manager

    Option B - Manual:

    • After deployment, access the container shell via TrueNAS web UI
    • Navigate to Apps → Installed → borg-backup → Shell
    • Run: /scripts/init.sh
    • Backup your passphrase and the repository key to password manager
  4. Monitor Backups:

    • View logs: Apps → Installed → borg-backup → Logs
    • Backups will run automatically according to your cron schedule
⁠TrueNAS-Specific Notes

Timezone Configuration:

  • Do NOT add TZ as a manual environment variable in TrueNAS Custom Apps
  • TrueNAS automatically manages timezone - look for a Timezone dropdown in the app configuration
  • Adding TZ manually will cause deployment errors: Environment variable [TZ] is already defined

SSH Key Permissions:

  • Private key must be 600 (read/write for owner only)
  • SSH directory must be 700 (read/write/execute for owner only)
  • If permissions are incorrect, SSH authentication will silently fail

Testing SSH Connection:

Before deploying, verify SSH key authentication works:

ssh -i /mnt/pool/borg-backup/ssh/key -p <port> [email protected]

If prompted for password, SSH key is not configured correctly on remote server.

⁠Docker Compose Setup
  1. Generate SSH keys (if you don't have them):

    mkdir -p ssh
    ssh-keygen -t ed25519 -f ssh/key -N ""
    
  2. Add the public key to your backup server:

    cat ssh/key.pub
    # Copy this and add it to ~/.ssh/authorized_keys on your backup server
    
  3. Configure environment:

    cp .env.example .env
    # Edit .env with your settings
    
  4. Initialize the Borg repository (one-time):

    docker compose run --rm borg-backup /scripts/init.sh
    
  5. Start the backup container:

    docker compose up -d
    

⁠Configuration Reference

⁠Environment Variables
VariableRequiredDefaultDescription
BORG_REPOYes-Full SSH URL to repository (e.g., ssh://user@host:22/~/backup)
BORG_PASSPHRASEYes*-Repository encryption passphrase. *One of BORG_PASSPHRASE, BORG_PASSPHRASE_FILE, or BORG_PASSCOMMAND is required.
BORG_PASSPHRASE_FILENo-Path to a file containing the passphrase (read at startup). Mount from an encrypted dataset to keep the secret off the apps pool.
BORG_PASSCOMMANDNo-Command that prints the passphrase (e.g. cat /run/secrets/passphrase). Borg-native; keeps the secret out of the environment entirely.
BACKUP_PATHSYes-Colon-separated paths to back up (e.g., /data/photos:/data/docs)
BACKUP_EXCLUDESNo-Colon-separated paths/patterns to exclude (e.g., /data/photos/cache:/data/docs/tmp)
BORG_RSHNossh -i /ssh/key -o StrictHostKeyChecking=accept-newSSH command
CRON_SCHEDULENo-Cron expression for scheduled backups (e.g., 0 2 * * 0). Omit to run on-demand only.
RUN_ON_STARTNofalseRun backup immediately on container start
AUTO_INITNofalseAutomatically initialize repository if it doesn't exist
PRUNE_KEEP_DAILYNo7Daily archives to keep
PRUNE_KEEP_WEEKLYNo4Weekly archives to keep
PRUNE_KEEP_MONTHLYNo6Monthly archives to keep
TZNoUTCTimezone for cron jobs
VERIFY_ENABLEDNofalseEnable scheduled repository integrity verification
VERIFY_REPO_CRON_SCHEDULENo-Repository check schedule (e.g., 0 3 * * 0 for weekly Sunday 03:00)
VERIFY_ARCHIVES_CRON_SCHEDULENo-Archives check schedule (e.g., 0 3 1 * * for monthly 1st at 03:00)
VERIFY_LEVELNorepositoryManual verification depth: repository, archives, or full
VERIFY_LOCK_WAITNo300Seconds a check waits for the repository lock before skipping (it never breaks the lock)
PREFLIGHT_ENABLEDNotrueRun the startup recovery-readiness report
PREFLIGHT_STRICTNofalseRefuse to start if preflight finds a problem (e.g. a wrong passphrase)
⁠Restore Drill Variables (Optional)
VariableRequiredDefaultDescription
RESTORE_DRILL_ENABLEDNofalseEnable scheduled restore drills
RESTORE_DRILL_CRON_SCHEDULENo-Drill schedule (e.g. 0 4 1 1,4,7,10 * for quarterly)
RESTORE_DRILL_SAMPLE_COUNTNo3How many files to restore per drill
RESTORE_DRILL_PATHSNo-Colon-separated archive paths to drill instead of a rotating sample (e.g. data/db/dump.sql)
RESTORE_DRILL_MAX_FILE_BYTESNo104857600Skip files larger than this when sampling (default 100 MB)
RESTORE_DRILL_ARCHIVENolatestArchive to drill
RESTORE_DRILL_TARGETNo/tmp/restore-drillScratch directory for drills. Each run works in its own subdirectory, so other files here are untouched. Must be outside BACKUP_PATHS.
RESTORE_DRILL_KEEPNofalseKeep restored files after the drill for inspection
RESTORE_DRILL_LOCK_WAITNo60Seconds to wait for the repository lock before skipping the drill
RESTORE_DRILL_STATE_FILENo/borg/config/last-restore-drillWhere the last successful drill is recorded
RESTORE_DRILL_MAX_AGE_DAYSNo100Preflight warns if the last successful drill is older than this
⁠Job History Variables (Optional)

Every job records its outcome to a persistent file, so a failure survives log rotation, a redeploy and an app restart.

VariableRequiredDefaultDescription
HISTORY_FILENo/borg/config/history.logWhere job events are recorded
HISTORY_MAX_LINESNo500Rolling cap; oldest lines are dropped

Recorded events: backup.success, backup.failure, prune.success, prune.failure, verify.success, verify.failure, restore.success, restore.failure, container.startup, container.shutdown

See Job History⁠.

⁠Backup Time Window and Rate Limiting (Optional)
VariableRequiredDefaultDescription
BACKUP_WINDOW_STARTNo-Start of backup window in HH:MM format (e.g., 01:00)
BACKUP_WINDOW_ENDNo-End of backup window in HH:MM format (e.g., 07:00)
BACKUP_RATE_LIMIT_IN_WINDOWNo-1Rate limit during window in Mbps (-1 = unlimited)
BACKUP_RATE_LIMIT_OUT_WINDOWNo-1Rate limit outside window in Mbps (0 = stopped, -1 = unlimited)

Rate Limit Values:

  • -1 = Unlimited bandwidth (burst speeds)
  • 0 = Terminate backup outside window, auto-resume from checkpoint (only valid for BACKUP_RATE_LIMIT_OUT_WINDOW)
  • Positive number = Bandwidth limit in Mbps (e.g., 40 = 40 Mbps)

Use Cases:

  1. Large initial backup on limited connection (e.g., 1.5TB on 40 Mbps):

    BACKUP_WINDOW_START=01:00
    BACKUP_WINDOW_END=07:00
    BACKUP_RATE_LIMIT_IN_WINDOW=-1    # Unlimited overnight
    BACKUP_RATE_LIMIT_OUT_WINDOW=0    # Terminated and resumed via checkpoint
    
    • Backup runs at full speed during 1am-7am window
    • Automatically terminated at 7am (checkpoint polling in final 30 min)
    • Resumes next night from last checkpoint
    • Completes 1.5TB in 3-4 nights with minimal rework
  2. Continuous backup with daytime throttle:

    BACKUP_WINDOW_START=22:00
    BACKUP_WINDOW_END=08:00
    BACKUP_RATE_LIMIT_IN_WINDOW=-1    # Unlimited overnight
    BACKUP_RATE_LIMIT_OUT_WINDOW=5    # 5 Mbps trickle during day
    
  3. Daytime backup with bandwidth limit:

    BACKUP_WINDOW_START=09:00
    BACKUP_WINDOW_END=17:00
    BACKUP_RATE_LIMIT_IN_WINDOW=20    # 20 Mbps during business hours
    BACKUP_RATE_LIMIT_OUT_WINDOW=0    # Terminated outside business hours
    

How It Works:

  • Borg (1.1+) automatically creates checkpoints every 30 minutes during backup
  • When backup window ends with BACKUP_RATE_LIMIT_OUT_WINDOW=0:
    • Monitor polls for new checkpoints in final 30 minutes of window
    • If new checkpoint detected → backup terminated immediately (minimizes wasted work)
    • If window ends → backup terminated at deadline (hard stop)
    • Zero out-of-window bandwidth usage (never exceeds window)
  • Next backup run automatically resumes from last checkpoint
  • Container restarts automatically break stale locks and resume from checkpoint
  • No manual intervention required

Requirements:

  • Borg 1.1 or later (for checkpoint support within files)
⁠Repository Integrity Verification (Optional)

Scheduled borg check verification ensures your backup repository remains healthy and detects corruption early. When enabled, two verification jobs are configured automatically:

  • Repository check (weekly) - Fast verification of repository structure
  • Archives check (monthly) - Deeper verification of archive metadata integrity
VariableRequiredDefaultDescription
VERIFY_ENABLEDNofalseEnable scheduled verification
VERIFY_REPO_CRON_SCHEDULENo-Repository check schedule (e.g., 0 3 * * 0 for weekly)
VERIFY_ARCHIVES_CRON_SCHEDULENo-Archives check schedule (e.g., 0 3 1 * * for monthly)
VERIFY_LOCK_WAITNo300Seconds to wait for the repository lock before skipping the check

Verification Levels:

LevelCommandSpeedUse Case
repository--repository-onlyFastWeekly scheduled checks - verifies repository structure
archives--archives-onlyMediumMonthly checks - verifies archive metadata integrity
full--verify-dataVery slowManual spot-checks only - reads and verifies all data

Example Configuration:

VERIFY_ENABLED=true
VERIFY_REPO_CRON_SCHEDULE=0 6 * * 0      # Weekly Sunday 6am
VERIFY_ARCHIVES_CRON_SCHEDULE=0 6 1 * *  # Monthly 1st at 6am

Leave room after your backup. A check needs the repository lock, and a running backup holds it, so a check that fires while the backup is still going waits VERIFY_LOCK_WAIT seconds and then skips. With a nightly backup at 01:00 that takes two hours, a check at 03:00 skips every week; 06:00 leaves headroom for the backup to grow.

Behaviour Notes:

  • A check never interrupts a running backup. It waits up to VERIFY_LOCK_WAIT seconds (300 by default) for the repository lock, then skips with a verify.skipped entry in the job history and runs on the next schedule. It does not break the lock
  • A skipped check is reported on every container start by the startup preflight, so a check that keeps skipping does not go unnoticed
  • Verification is read-only and does not respect backup windows (no bandwidth impact)
  • full level reads all repository data and is very slow on large repos - use for manual spot-checks only
  • No double-runs: If the archives day falls on a repo day (e.g., 1st is a Sunday), only the archives check runs - the repository check is automatically skipped

Manual Verification:

# Quick repository check
docker compose run --rm borg-backup /scripts/verify.sh

# Archives check
docker compose run --rm -e VERIFY_LEVEL=archives borg-backup /scripts/verify.sh

# Full data verification (slow - use for spot checks)
docker compose run --rm -e VERIFY_LEVEL=full borg-backup /scripts/verify.sh
⁠Volume Mounts
Container PathPurposeMode
/dataSource directories to back upread-only
/sshSSH private keyread-only
/borg/cacheBorg cache (improves performance)read-write
/borg/configBorg config persistenceread-write
/restoreDestination for restored files (optional)read-write

⁠Recovery

A backup you have never restored is a hypothesis, not a backup.

This container treats recovery as a first-class operation: the same scripts that take backups can list, inspect, verify and restore them, and can prove on a schedule that restoring still works.

⁠Recovery Quick Reference

Every command below runs inside the container. On TrueNAS use Apps → Installed → borg-backup → Shell; with Compose use docker compose exec borg-backup <command> (or run --rm on a stopped stack).

GoalCommand
What archives exist?/scripts/restore.sh list
Name of the newest archive/scripts/restore.sh latest
What is in an archive?/scripts/restore.sh info latest
Find a file inside an archive/scripts/restore.sh files latest holiday.jpg
Prove an archive is readable (writes nothing)/scripts/restore.sh dry-run latest
Restore everything/scripts/restore.sh extract latest /restore
Restore one file/scripts/restore.sh extract latest /restore data/photos/holiday.jpg
Browse an archive like a filesystem/scripts/restore.sh mount latest /mnt/backup
Stop browsing/scripts/restore.sh umount /mnt/backup
Check repository integrity/scripts/restore.sh check
Prove recovery works right now/scripts/restore.sh drill
Export the key for disaster recovery/scripts/restore.sh key-export

Any archive name may be given as latest, which resolves to the newest archive (Borg 1.x has no ::latest pseudo-archive, so the container resolves it for you).

⁠Restoring Files

Restores never write to your source data. Extract into a dedicated directory and move files back yourself once you have checked them.

# Everything, into a restore volume
docker compose run --rm -v /mnt/pool/borg-restore:/restore borg-backup \
  /scripts/restore.sh extract latest /restore

# Just the paths you need - far quicker on a large repository
docker compose run --rm -v /mnt/pool/borg-restore:/restore borg-backup \
  /scripts/restore.sh extract latest /restore data/documents/tax-2025.pdf

Paths inside an archive are stored without a leading slash: a source path of /data/documents appears in the archive as data/documents. Use /scripts/restore.sh files latest <pattern> to find the exact path.

Built-in fail-safes. extract refuses to run if the destination is / or sits inside any configured BACKUP_PATHS, because Borg would recreate the original tree there and overwrite the very data you are recovering. It also warns when the destination is not empty.

⁠Browsing an Archive (FUSE Mount)

Mounting is the cheapest way to pick a few files out of a multi-terabyte archive: Borg fetches only the data you actually read.

Mounting needs kernel FUSE access, which Docker does not grant by default:

services:
  borg-backup:
    devices:
      - "/dev/fuse"
    cap_add:
      - "SYS_ADMIN"
    security_opt:
      - "apparmor=unconfined"
docker run --rm -it --device /dev/fuse --cap-add SYS_ADMIN \
  --security-opt apparmor=unconfined \
  -e BORG_REPO=... -e BORG_PASSPHRASE=... \
  -v ./ssh:/ssh:ro diarmuidk/docker-borg-client \
  /scripts/restore.sh mount latest /mnt/backup

Without those options restore.sh mount exits with instructions rather than a bare FUSE mount failed. If you cannot grant them, use extract instead - it needs no special privileges.

⁠Restore Drills: Proving Recoverability

borg check verifies that the repository is structurally sound. It does not verify that you can get your files back. A restore drill does:

  1. Resolves the archive to drill (newest by default).
  2. Picks a sample of real files from it, rotating the selection by day so successive drills cover different data.
  3. Restores them to a temporary directory.
  4. Compares each restored file against the live source byte-for-byte (SHA-256) when the source still exists.
  5. Reports a summary and writes a restore.success / restore.failure entry to the job history⁠.
RESTORE_DRILL_ENABLED=true
RESTORE_DRILL_CRON_SCHEDULE=0 4 1 1,4,7,10 *   # Quarterly, 1st at 04:00
RESTORE_DRILL_SAMPLE_COUNT=3

Run one on demand at any time:

docker compose exec borg-backup /scripts/restore.sh drill

Pin the drill to the files t

Tag summary

Content type

Image

Digest

sha256:96c1e3d2d…

Size

27.5 MB

Last updated

13 days ago

docker pull diarmuidk/docker-borg-client