A minimal, generic Docker container for running BorgBackup backups.
3.6K
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).
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:
Perfect for:
TrueNAS SCALE users can deploy this container using the Custom App feature:
Prepare 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
cat /mnt/pool/borg-backup/ssh/key.pub
Deploy Custom App:
Application Name: borg-backup
Image Configuration:
diarmuidk/docker-borg-clientlatestAlwaysContainer 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:
/mnt/pool/borg-backup/ssh/sshAdd Host Path Volume for each dataset to backup:
/mnt/pool/your-dataset/data/dataset1Add ixVolume for Borg cache:
/borg/cacheAdd ixVolume for Borg config:
/borg/configRestart Policy: Unless Stopped
Initialize Repository:
Option A - Automatic (Recommended):
AUTO_INIT=true to environment variablescat /borg/config/repo-key.txt and save to password managerOption B - Manual:
/scripts/init.shMonitor Backups:
Timezone Configuration:
TZ as a manual environment variable in TrueNAS Custom AppsTZ manually will cause deployment errors: Environment variable [TZ] is already definedSSH Key Permissions:
600 (read/write for owner only)700 (read/write/execute for owner only)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.
Generate SSH keys (if you don't have them):
mkdir -p ssh
ssh-keygen -t ed25519 -f ssh/key -N ""
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
Configure environment:
cp .env.example .env
# Edit .env with your settings
Initialize the Borg repository (one-time):
docker compose run --rm borg-backup /scripts/init.sh
Start the backup container:
docker compose up -d
| Variable | Required | Default | Description |
|---|---|---|---|
BORG_REPO | Yes | - | Full SSH URL to repository (e.g., ssh://user@host:22/~/backup) |
BORG_PASSPHRASE | Yes* | - | Repository encryption passphrase. *One of BORG_PASSPHRASE, BORG_PASSPHRASE_FILE, or BORG_PASSCOMMAND is required. |
BORG_PASSPHRASE_FILE | No | - | Path to a file containing the passphrase (read at startup). Mount from an encrypted dataset to keep the secret off the apps pool. |
BORG_PASSCOMMAND | No | - | Command that prints the passphrase (e.g. cat /run/secrets/passphrase). Borg-native; keeps the secret out of the environment entirely. |
BACKUP_PATHS | Yes | - | Colon-separated paths to back up (e.g., /data/photos:/data/docs) |
BACKUP_EXCLUDES | No | - | Colon-separated paths/patterns to exclude (e.g., /data/photos/cache:/data/docs/tmp) |
BORG_RSH | No | ssh -i /ssh/key -o StrictHostKeyChecking=accept-new | SSH command |
CRON_SCHEDULE | No | - | Cron expression for scheduled backups (e.g., 0 2 * * 0). Omit to run on-demand only. |
RUN_ON_START | No | false | Run backup immediately on container start |
AUTO_INIT | No | false | Automatically initialize repository if it doesn't exist |
PRUNE_KEEP_DAILY | No | 7 | Daily archives to keep |
PRUNE_KEEP_WEEKLY | No | 4 | Weekly archives to keep |
PRUNE_KEEP_MONTHLY | No | 6 | Monthly archives to keep |
TZ | No | UTC | Timezone for cron jobs |
VERIFY_ENABLED | No | false | Enable scheduled repository integrity verification |
VERIFY_REPO_CRON_SCHEDULE | No | - | Repository check schedule (e.g., 0 3 * * 0 for weekly Sunday 03:00) |
VERIFY_ARCHIVES_CRON_SCHEDULE | No | - | Archives check schedule (e.g., 0 3 1 * * for monthly 1st at 03:00) |
VERIFY_LEVEL | No | repository | Manual verification depth: repository, archives, or full |
VERIFY_LOCK_WAIT | No | 300 | Seconds a check waits for the repository lock before skipping (it never breaks the lock) |
PREFLIGHT_ENABLED | No | true | Run the startup recovery-readiness report |
PREFLIGHT_STRICT | No | false | Refuse to start if preflight finds a problem (e.g. a wrong passphrase) |
| Variable | Required | Default | Description |
|---|---|---|---|
RESTORE_DRILL_ENABLED | No | false | Enable scheduled restore drills |
RESTORE_DRILL_CRON_SCHEDULE | No | - | Drill schedule (e.g. 0 4 1 1,4,7,10 * for quarterly) |
RESTORE_DRILL_SAMPLE_COUNT | No | 3 | How many files to restore per drill |
RESTORE_DRILL_PATHS | No | - | Colon-separated archive paths to drill instead of a rotating sample (e.g. data/db/dump.sql) |
RESTORE_DRILL_MAX_FILE_BYTES | No | 104857600 | Skip files larger than this when sampling (default 100 MB) |
RESTORE_DRILL_ARCHIVE | No | latest | Archive to drill |
RESTORE_DRILL_TARGET | No | /tmp/restore-drill | Scratch directory for drills. Each run works in its own subdirectory, so other files here are untouched. Must be outside BACKUP_PATHS. |
RESTORE_DRILL_KEEP | No | false | Keep restored files after the drill for inspection |
RESTORE_DRILL_LOCK_WAIT | No | 60 | Seconds to wait for the repository lock before skipping the drill |
RESTORE_DRILL_STATE_FILE | No | /borg/config/last-restore-drill | Where the last successful drill is recorded |
RESTORE_DRILL_MAX_AGE_DAYS | No | 100 | Preflight warns if the last successful drill is older than this |
Every job records its outcome to a persistent file, so a failure survives log rotation, a redeploy and an app restart.
| Variable | Required | Default | Description |
|---|---|---|---|
HISTORY_FILE | No | /borg/config/history.log | Where job events are recorded |
HISTORY_MAX_LINES | No | 500 | Rolling 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.
| Variable | Required | Default | Description |
|---|---|---|---|
BACKUP_WINDOW_START | No | - | Start of backup window in HH:MM format (e.g., 01:00) |
BACKUP_WINDOW_END | No | - | End of backup window in HH:MM format (e.g., 07:00) |
BACKUP_RATE_LIMIT_IN_WINDOW | No | -1 | Rate limit during window in Mbps (-1 = unlimited) |
BACKUP_RATE_LIMIT_OUT_WINDOW | No | -1 | Rate 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)40 = 40 Mbps)Use Cases:
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
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
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:
BACKUP_RATE_LIMIT_OUT_WINDOW=0:
Requirements:
Scheduled borg check verification ensures your backup repository remains healthy and detects corruption early. When enabled, two verification jobs are configured automatically:
| Variable | Required | Default | Description |
|---|---|---|---|
VERIFY_ENABLED | No | false | Enable scheduled verification |
VERIFY_REPO_CRON_SCHEDULE | No | - | Repository check schedule (e.g., 0 3 * * 0 for weekly) |
VERIFY_ARCHIVES_CRON_SCHEDULE | No | - | Archives check schedule (e.g., 0 3 1 * * for monthly) |
VERIFY_LOCK_WAIT | No | 300 | Seconds to wait for the repository lock before skipping the check |
Verification Levels:
| Level | Command | Speed | Use Case |
|---|---|---|---|
repository | --repository-only | Fast | Weekly scheduled checks - verifies repository structure |
archives | --archives-only | Medium | Monthly checks - verifies archive metadata integrity |
full | --verify-data | Very slow | Manual 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:
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 lockfull level reads all repository data and is very slow on large repos - use for manual spot-checks onlyManual 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
| Container Path | Purpose | Mode |
|---|---|---|
/data | Source directories to back up | read-only |
/ssh | SSH private key | read-only |
/borg/cache | Borg cache (improves performance) | read-write |
/borg/config | Borg config persistence | read-write |
/restore | Destination for restored files (optional) | read-write |
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.
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).
| Goal | Command |
|---|---|
| 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).
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.
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.
borg check verifies that the repository is structurally sound. It does not
verify that you can get your files back. A restore drill does:
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
Content type
Image
Digest
sha256:96c1e3d2d…
Size
27.5 MB
Last updated
13 days ago
docker pull diarmuidk/docker-borg-client