====== Borg Backup - essential commands ======
Deduplicating, compressing, optionally encrypted backups. Verified against **borg 1.2.9**.
Two things that trip everyone up on day one:
* Archive paths are stored **without the leading slash** — you extract ''%%etc/fstab%%'', not ''%%/etc/fstab%%''.
* ''%%borg mount%%'' holds a **lock** on the repository. While mounted, ''%%borg create%%'' cannot run.
===== Setup =====
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.
===== Creating archives =====
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. |
===== Inspecting =====
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/
===== Restoring =====
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
===== Retention =====
''%%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.
===== Integrity =====
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
===== Housekeeping and troubleshooting =====
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%%''. |
===== Environment variables =====
^ 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**, ''%%HOME%%'' is not set unless you set it. Borg then builds a second files cache and re-reads everything from scratch on every run. Add ''%%Environment=HOME=/root%%'' to the unit.
===== Cron / systemd skeleton =====
#!/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.
===== See also =====
* [[https://borgbackup.readthedocs.io/|Official documentation]]
* [[linux:check-hdd-health|Check HDD health]]