Deduplicating, compressing, optionally encrypted backups. Verified against borg 1.2.9.
Two things that trip everyone up on day one:
etc/fstab, not /etc/fstab.borg mount holds a lock on the repository. While mounted, borg create cannot run.
Point BORG_REPO at the repository once and every command gets shorter — ::name is enough afterwards.
export BORG_REPO=/mnt/backup/borg # local export BORG_REPO=ssh://user@host/./borg # remote (./ = relative to home)
Create the repository. The encryption mode is fixed at creation and cannot be changed later:
borg init --encryption=repokey-blake2 :: # key in repo, unlocked by passphrase borg init --encryption=keyfile-blake2 :: # key stays on this machine only borg init --encryption=none :: # no key, no passphrase
borg key export :: borg-key.txt # BACK THIS UP, off-machine borg key export --paper :: # printable version
Losing the key or passphrase of an encrypted repository means losing the backup. There is no recovery.
borg create --stats --progress ::'{hostname}-{now}' /etc /home /srv
Placeholders usable in archive names: {hostname}, {user}, {now}, {utcnow}, {pid}.
Realistic invocation:
borg create \
--one-file-system \
--compression zstd,3 \
--exclude-from /etc/borg.excl \
--exclude-caches \
--stats \
::'{hostname}-{now}' \
/ /boot /home /data
Preview what would be archived, without writing anything:
borg create --dry-run --list ::test / | head -50
Back up the output of a command instead of a file:
mysqldump --all-databases | borg create ::db-'{now}' -
| Flag | Why you want it |
|---|---|
--one-file-system | Do not cross mount points. Keeps the backup target itself out of the archive. |
--exclude-caches | Skips directories tagged CACHEDIR.TAG. |
--exclude-if-present .nobackup | Opt-out marker you can drop anywhere. |
--compression zstd,3 | Good default. lz4 = fastest, zstd,10 = smaller/slower, none. |
--stats | Prints original / compressed / deduplicated sizes. |
--checkpoint-interval 900 | Resume point every 15 min on long runs. |
borg list # archives in the repository borg list ::archive-name # files inside one archive borg list ::archive-name 'var/log/**' # only matching paths borg info ::archive-name # size, duration, dedup stats borg info # totals for the whole repository
Custom output, useful for scripting:
borg list --format '{archive}{TAB}{time}{NL}' borg list ::arch --format '{size:8d} {path}{NL}' borg list --json | jq -r '.archives[].name'
What changed between two archives — the second one takes no repository prefix:
borg diff ::monday tuesday borg diff ::monday tuesday etc/
Paths are relative and extraction writes into the current directory, so cd first.
cd /tmp/restore borg extract ::archive-name # everything borg extract ::archive-name etc/nginx # one subtree borg extract ::archive-name 'home/*/.ssh' # pattern borg extract --dry-run --list ::archive-name etc/ # preview only borg extract --strip-components 2 ::arch var/www/site
Single file to stdout:
borg extract --stdout ::archive-name etc/fstab
Browse the archive like a normal folder — needs llfuse, mounted read-only:
borg mount ::archive-name /mnt/restore borg mount :: /mnt/restore # ALL archives, one subdirectory each ls /mnt/restore borg umount /mnt/restore # do not forget - it holds a lock
Export straight to tar, no intermediate extraction:
borg export-tar ::archive-name backup.tar.gz --tar-filter="gzip" borg export-tar ::archive-name - | tar tvf - | less
prune deletes archives; compact is what actually frees disk space.
borg prune --list --dry-run --keep-daily 7 --keep-weekly 4 --keep-monthly 12 borg prune --list --keep-daily 7 --keep-weekly 4 --keep-monthly 12 borg compact
Always dry-run first. Restrict to one host if several share the repository:
borg prune --glob-archives 'web01-*' --keep-daily 7 --keep-monthly 6
| Rule | Meaning |
|---|---|
--keep-last N | Last N archives, whatever their age. |
--keep-within 10d | Everything from the last 10 days. |
--keep-daily / -weekly / -monthly / -yearly N | Newest archive of each period. |
An archive matched by no rule is deleted. Running prune with no keep rule at all deletes everything.
borg check # structural: repository + archive metadata borg check --verify-data # re-reads and re-hashes every chunk (slow, thorough) borg check --repository-only # quick, repository layer only
Rough guidance: structural weekly, --verify-data monthly. The second one is the only check that catches silent bit rot on disks with no SMART.
borg check --repair # last resort, can discard data
borg delete ::archive-name # one archive borg delete --glob-archives 'tmp-*' --dry-run --list :: borg delete :: # the whole repository (asks for confirmation)
borg break-lock :: # stale lock after a crash - check nothing runs first borg delete --cache-only :: # rebuild the local cache borg with-lock :: cp -a repo /elsewhere
Retroactively drop files from existing archives (rewrites them):
borg recreate --list --dry-run --exclude '*/node_modules/*' :: borg recreate --recompress --compression zstd,10 ::
| Message | Cause |
|---|---|
Failed to create/acquire the lock (timeout) | Another borg is running, or the repository is still mounted. |
Cache is newer than repository | Repository was rolled back or restored. Run borg delete --cache-only. |
Repository ... does not exist | Wrong BORG_REPO, or the :: is missing. |
repository version not supported | Borg 2.x cannot read a 1.x repository. Use borg 1.x or borg transfer. |
| Variable | Use |
|---|---|
BORG_REPO | Default repository, lets you write ::archive. |
BORG_PASSPHRASE | Non-interactive runs. Prefer the next one. |
BORG_PASSCOMMAND | e.g. cat /root/.borg-pass — keeps the secret out of the environment. |
BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes | Skip the prompt for unencrypted repositories. |
BORG_RELOCATED_REPO_ACCESS_IS_OK | Repository moved path. Leave it no unless you moved it yourself. |
BORG_CACHE_DIR / BORG_CONFIG_DIR | Override ~/.cache/borg and ~/.config/borg. |
Under systemd,HOMEis not set unless you set it. Borg then builds a second files cache and re-reads everything from scratch on every run. AddEnvironment=HOME=/rootto the unit.
#!/bin/bash set -uo pipefail export BORG_REPO=/mnt/backup/borg export BORG_PASSCOMMAND='cat /root/.borg-pass' borg create --one-file-system --compression zstd,3 \ --exclude-from /etc/borg.excl --stats \ ::'{hostname}-{now}' / /home /data rc=$? borg prune --keep-daily 7 --keep-weekly 4 --keep-monthly 12 borg compact # 0 = ok, 1 = warnings (archive is still valid), 2 = error [ $rc -ge 2 ] && exit 1 exit 0
Exit codes: 0 success, 1 warning (archive written and usable), 2 error.