User Tools

Site Tools


linux:borg

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

linux/borg.txt · Last modified: by 127.0.0.1