How to back up a server with BorgBackup
BorgBackup (borg) makes deduplicated, compressed and encrypted backups into a repository on a second server that also runs Borg, reached over SSH. Create the repository once with borg init --encryption=repokey, export the key and keep it off both servers, then run borg create, borg prune and borg compact from cron. Borg 1.x does not write to S3 or other object storage itself.
What Borg does, and where backups live
Borg splits files into chunks and stores each chunk once, so a nightly backup of a mostly unchanged server adds only what changed. Chunks are compressed (lz4 by default) and, with the encryption modes below, encrypted and authenticated before they leave the server. The repository is a directory: on a local or mounted disk, or on another server where SSH starts borg serve. That server needs Borg installed, and sees neither your passphrase nor your files.
Borg 1.x cannot write to an S3 bucket. If you want backups in object storage without a second server, use restic or rclone. The Borg FAQ allows copying a repository elsewhere with a tool like rclone, but warns never to use the copy and the original as two live repositories.
This guide uses Borg 1.4, the stable series (1.4.5, July 2026). Borg 2.0 is still in beta (2.0.0b25, September 2026) and uses a new repository format that 1.x archives reach through borg transfer. Ubuntu 24.04's package is Borg 1.2.8, which has every command used here.
Install Borg on both servers
sudo apt install borgbackupRun it on the server being backed up and on the backup server, then check borg --version on each. For 1.4, the project publishes standalone binaries on its GitHub releases page; copy the one for your platform to /usr/local/bin/borg, owned by root with mode 755.
Prepare the backup server
On the backup server, create a user that owns the repositories:
sudo useradd --create-home --shell /bin/bash borgsudo -u borg mkdir -m 700 /home/borg/repos /home/borg/.sshOn the server being backed up (here web1), create an SSH key used only for Borg, and print its public half:
sudo ssh-keygen -t ed25519 -f /root/.ssh/borg_ed25519 -N '' -C 'borg-web1'sudo cat /root/.ssh/borg_ed25519.pubBack on the backup server, put it in /home/borg/.ssh/authorized_keys (owned by borg, mode 600) as one line:
command="borg serve --restrict-to-repository /home/borg/repos/web1",restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...rest-of-key borg-web1command= forces every login with this key to run borg serve, so the key cannot open a shell. --restrict-to-repository limits it to exactly that repository, which need not exist yet. restrict turns off port, agent and X11 forwarding and PTY allocation.
No second server? A Hetzner Storage Box runs Borg on its side: see how to use a Hetzner Storage Box for backups.
Create the encrypted repository
Back on web1, as root, store a random passphrase and tell Borg where everything is:
sudo sh -c 'head -c 32 /dev/urandom | base64 > /root/.borg-passphrase && chmod 600 /root/.borg-passphrase'export BORG_REPO='ssh://[email protected]/home/borg/repos/web1'
export BORG_RSH='ssh -i /root/.ssh/borg_ed25519'
export BORG_PASSCOMMAND='cat /root/.borg-passphrase'sudo -i. /root/.borg-envborg init --encryption=repokeyBORG_REPOis the default repository, so commands can omit it and archives are written::name. Use the same absolute path as inauthorized_keys.BORG_RSHreplacesssh, here to pick the Borg key.BORG_PASSCOMMANDruns a command and uses its output as the passphrase. It runs without a shell, so$HOMEworks but~does not.repokeystores the key inside the repository, encrypted with your passphrase; the Borg docs' rule of thumb isrepokeywith a strong passphrase. On CPUs without SHA-256 hardware acceleration,repokey-blake2is often faster. The mode cannot be changed later.
The first connection asks you to accept the backup server's SSH host key, so run borg init by hand before any cron job.
Export the key
Opening the repository takes both the key and the passphrase. With repokey the key lives in the repository, and a damaged key file there locks you out. Export it and store it, with the passphrase, somewhere that is neither server:
borg key export --paper "$BORG_REPO"Without the key and the passphrase, nobody can read the repository, you included. Keeping the only copy of the passphrase in /root/.borg-passphrase means losing web1 loses the backups too. Put both in a password manager.
The Borg docs note that if the passphrase is stored on the server being backed up, --encryption=keyfile gives the same or better security: the key stays in ~/.config/borg/keys on the client, so the repository alone is useless to an attacker. You must then back up that key file, since the repository no longer holds it.
Create a backup
borg create --stats --compression zstd,3 --one-file-system --exclude-caches --exclude-from /etc/borg-excludes ::'{hostname}-files-{now:%Y-%m-%d_%H%M}' /etc /var/www /root::'{hostname}-files-{now:%Y-%m-%d_%H%M}'names the archive, hereweb1-files-2026-10-03_0230. Names must be unique. The quotes stop the shell touching the braces.--compression zstd,3uses zstd at level 3. lz4 (the default) is faster with less compression;auto,zstd,7skips data that does not compress.--one-file-systemskips mount points below the listed paths.--exclude-cachesskips directories holding aCACHEDIR.TAGfile.--statsprints sizes at the end. This archive's deduplicated size is how much the repository grew.
# fnmatch patterns, matched against paths without the leading /
*.log
home/*/.cache
root/.cache
var/www/*/cacheBorg stores /var/www as var/www and strips a leading / from patterns. In this default pattern style * also matches across /, and a pattern that matches a directory excludes everything in it. Test patterns with borg create --list --dry-run. Borg reads each file as it finds it, so dump databases rather than copying their files. --content-from-command saves a command's output and fails the archive if the command fails:
borg create --stdin-name mydb.sql --content-from-command ::'{hostname}-db-{now:%Y-%m-%d_%H%M}' -- sudo -u postgres pg_dump mydbBorg exits 0 on success, 1 on warnings (a file that changed or could not be read during the run) and 2 on errors.
Prune and compact old archives
borg prune --list --glob-archives '{hostname}-files-*' --keep-daily 7 --keep-weekly 4 --keep-monthly 6borg compact- Without
--glob-archives, prune considers every archive in the repository as one series. Give file and database archives different prefixes and prune each with its own glob, or--keep-daily 7might keep a day's database dump and drop that day's files. --keep-daily 7keeps the last archive of each of the 7 most recent days with backups; weekly and monthly work the same way. Add--dry-runto preview.- Prune only marks data as deleted.
borg compactfrees the space. It needs no key, so it can run on the backup server too.
Check and test
borg checkThis checks the repository's segments by size and CRC, on the backup server so little crosses the network, then the archives' metadata on the client. borg check --verify-data also reads, decrypts and verifies every chunk: thorough, and slow. To prove a restore works without writing anything, extract to nowhere:
borg extract --dry-run ::web1-files-2026-10-03_0230List and restore
borg listborg list ::web1-files-2026-10-03_0230 var/www/siteThe first lists archives, the second files in one archive under a path. borg extract always writes into the current directory, so work in an empty one. Paths have no leading slash:
mkdir -p /srv/restore && cd /srv/restoreborg extract ::web1-files-2026-10-03_0230 var/www/site/wp-config.phpTo browse instead, mount an archive. On Ubuntu this needs the python3-pyfuse3 package. The mount does not restore special file flags or ACLs, so use extract for full restores:
borg mount ::web1-files-2026-10-03_0230 /mnt/borgborg umount /mnt/borgA database archive restores with borg extract --stdout ::web1-db-2026-10-03_0230 > mydb.sql. Then follow how to test a backup restore.
Make the repository append-only
With the setup above, an attacker with root on web1 can run borg delete against its backups. Give web1's key --append-only on the backup server:
command="borg serve --append-only --restrict-to-repository /home/borg/repos/web1",restrict ssh-ed25519 AAAA...web1-key borg-web1
command="borg serve --restrict-to-repository /home/borg/repos/web1",restrict ssh-ed25519 AAAA...admin-key borg-adminIn append-only mode, prune and delete still run and remove archives from the listing, but Borg never overwrites or deletes committed data, and borg compact compacts nothing, without a warning. The repository keeps a transaction log, and you can roll it back to before an attack, as long as borg compact has not run since. The second key, kept on a trusted machine, runs prune and compact. Check borg list before each compact: compacting after an attacker's deletes makes them permanent.
Automate it with cron
#!/bin/sh
. /root/.borg-env
borg create --stats --compression zstd,3 --one-file-system --exclude-caches \
--exclude-from /etc/borg-excludes \
::'{hostname}-files-{now:%Y-%m-%d_%H%M}' /etc /var/www /root
create_rc=$?
borg prune --list --glob-archives '{hostname}-files-*' \
--keep-daily 7 --keep-weekly 4 --keep-monthly 6
prune_rc=$?
borg compact
compact_rc=$?
rc=$(( create_rc > prune_rc ? create_rc : prune_rc ))
rc=$(( compact_rc > rc ? compact_rc : rc ))
exit "$rc"sudo chmod 700 /usr/local/bin/borg-backup.sh30 2 * * * root /usr/local/bin/borg-backup.sh >> /var/log/borg-backup.log 2>&1The script exits with the worst of the three return codes, so 1 means warnings and 2 means something failed. With an append-only key, drop the prune and compact steps and run them from the admin machine. More on schedules in how to schedule backups with cron.
Common errors
| Error | Fix |
|---|---|
Failed to create/acquire the lock ... (timeout). | Another Borg run is active, or a dead SSH session still holds the lock. Borg waits only 1 second by default; add --lock-wait 600. Run borg break-lock only when no Borg process on any machine uses the repository. |
Connection closed by remote host. Is borg working on the server? | Borg is missing on the backup server or not on its PATH (write the full path, such as /usr/local/bin/borg serve, in authorized_keys), or that line is malformed. |
Repository path not allowed | The path in BORG_REPO does not match --restrict-to-repository exactly. |
Passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND, or via BORG_PASSPHRASE_FD is incorrect. | Wrong passphrase file. Check BORG_PASSCOMMAND and that its command runs as root. |
Repository ... does not exist. | Run borg init first, or fix the path in BORG_REPO. |
| Repository disk keeps growing | borg compact never ran, or the key is append-only. Compact from the admin key. |
Frequently asked questions
- Can Borg back up directly to S3?
- No. Borg 1.x writes to a local or mounted directory, or to a server running borg serve over SSH. For S3-compatible storage, use restic or rclone.
- What is the difference between borg prune and borg compact?
- prune deletes archives that fall outside your retention rules. compact rewrites the repository's segment files to actually free the space; since Borg 1.2 it is a separate step.
- Which Borg encryption mode should I use?
- The docs' rule of thumb is repokey with a strong passphrase, or repokey-blake2 on CPUs without SHA-256 acceleration. Export the key either way.
- What does Borg append-only mode protect against?
- A compromised client cannot permanently delete backups: its deletes are not compacted, and the repository can be rolled back, provided nobody runs borg compact first.
- Should I use Borg 2.0?
- In October 2026 it is still a beta. Use the 1.4 series for production backups.
How this was checked
Commands, limits and prices were checked against these official pages, on October 3, 2026:
- Borg documentation: Quick Start
- Borg documentation: Installation
- Borg documentation: borg init
- Borg documentation: borg create
- Borg documentation: borg prune
- Borg documentation: borg compact
- Borg documentation: borg check
- Borg documentation: borg list
- Borg documentation: borg extract
- Borg documentation: borg mount
- Borg documentation: borg key
- Borg documentation: borg serve
- Borg documentation: Additional notes (append-only mode)
- Borg documentation: General (repository URLs, environment variables)
- Borg documentation: borg help (patterns, placeholders, compression)
- Borg documentation: Hosting repositories
- Borg documentation: FAQ