====== 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]]