How to automate BorgBackup with borgmatic to a second server
Back up files and databases to a second server over SSH with borgmatic: an append-only key, one YAML config, retention, checks and a daily timer.
Tested on Ubuntu 24.04 LTS
Before you start
- A server running Ubuntu 24.04 to back up
- A second Linux server with SSH and enough disk for the backups
- Root or a user with sudo on both
To automate BorgBackup, install borgmatic and borgbackup, describe what to back up, where to send it, how long to keep it and which databases to dump in one file, /etc/borgmatic/config.yaml, and turn on the systemd timer that ships with the package. Point it at a second server reached over SSH with a key that may only run borg serve --append-only in one directory, and a break-in on the first server can't delete its own backups.
Our guide to backups you should run shows a plain borg create script. borgmatic replaces that script: it runs create, prune, compact and check in the right order, dumps the databases into the archive without temporary files filling your disk, and restores them with one command.
The examples use these names:
| What | In this tutorial |
|---|---|
| The server you back up | web1 |
| The backup server | backup.example.com, address 203.0.113.20 |
| The account on the backup server | borg, with repositories under /home/borg/repos |
The repository for web1 |
/home/borg/repos/web1 |
Commands marked on web1 run there as root (sudo -i); commands marked on the backup server run there.
Install Borg on both servers
On the backup server and on web1:
sudo apt update
sudo apt install -y borgbackupOn web1 also install borgmatic:
sudo apt install -y borgmatic
borgmatic --version
borg --versionUbuntu 24.04 installs borgmatic 1.8.3 and Borg 1.2.8. Both servers should run the same Borg major version (1.x); minor differences are fine. On Debian 13 the package is borgmatic 1.9, which reads the same configuration. Debian 12's borgmatic 1.7 uses an older layout with location:, storage: and retention: sections, so on Debian 12 install a newer borgmatic with pipx or adapt the file.
Prepare the backup server
On the backup server, create an account that does nothing but hold repositories:
sudo adduser --disabled-password --gecos '' borg
sudo -u borg mkdir -p /home/borg/repos/web1 /home/borg/.ssh
sudo -u borg chmod 700 /home/borg/.ssh--disabled-password means nobody can log in with a password; the only way in will be the key you add next.
Give web1 an append-only key
On web1, make a key used only for backups:
ssh-keygen -t ed25519 -N '' -C 'borg@web1' -f /root/.ssh/borg_ed25519
cat /root/.ssh/borg_ed25519.pubOn the backup server, add that public key to /home/borg/.ssh/authorized_keys on one line, with the restrictions in front of it:
command="borg serve --append-only --restrict-to-path /home/borg/repos/web1",restrict ssh-ed25519 AAAAC3... borg@web1sudo chown borg:borg /home/borg/.ssh/authorized_keys
sudo chmod 600 /home/borg/.ssh/authorized_keysWhat the restrictions do:
command="borg serve ..."runs Borg's server whatever the client asks for. The key can't open a shell or run any other command.--restrict-to-pathlimits it to web1's own repository. Asking for another one fails withRepository path not allowed.--append-onlymeans the key can add archives but can't really remove anything. A prune or delete sent through it only writes a deletion record to the repository's log; the data stays on disk until you compact the repository from the backup server itself (the last step of this tutorial).restrictturns off port forwarding, agent forwarding and terminal allocation.
Each server you back up gets its own key, its own line and its own directory.
On web1, accept the backup server's host key once, so unattended runs don't stop at the question:
ssh-keyscan -t ed25519 backup.example.com >> /root/.ssh/known_hostsCompare the fingerprint with the one the backup server shows for ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub.
Check it worked
ssh -i /root/.ssh/borg_ed25519 borg@backup.example.com lsInstead of a file list you get Borg's usage text: the key reached the server, and the only thing it can do is run borg serve.
Write the borgmatic configuration
On web1, create the passphrase that encrypts the repository:
mkdir -p /etc/borgmatic
chmod 700 /etc/borgmatic
openssl rand -base64 32 > /etc/borgmatic/passphrase
chmod 600 /etc/borgmatic/passphraseThen /etc/borgmatic/config.yaml:
source_directories:
- /etc
- /home
- /root
- /var/www
repositories:
- path: ssh://borg@backup.example.com/./repos/web1
label: offsite
exclude_patterns:
- /root/.cache
- /home/*/.cache
- '*/node_modules'
exclude_caches: true
encryption_passcommand: cat /etc/borgmatic/passphrase
compression: zstd,3
ssh_command: ssh -i /root/.ssh/borg_ed25519
keep_daily: 7
keep_weekly: 4
keep_monthly: 6
checks:
- name: repository
frequency: 2 weeks
- name: archives
frequency: 4 weeks
postgresql_databases:
- name: appdb
username: postgres
mariadb_databases:
- name: allchmod 600 /etc/borgmatic/config.yaml
borgmatic config validateThe last command prints All configuration files are valid. What the settings do:
source_directories: what you made, not the operating system. Add/srv,/opt/yourappor Docker volumes if your apps keep data there.path: ssh://borg@backup.example.com/./repos/web1: the./makes the path relative to theborguser's home, so it is/home/borg/repos/web1.encryption_passcommand: borgmatic reads the passphrase from the root-only file, so it is never in the config or the process list.keep_*: the retention policy forprune: the newest archive of each of the last 7 days, 4 weeks and 6 months.checks: a repository check every two weeks and a full archive check every four, instead of on every run.postgresql_databasesandmariadb_databases: dumped at backup time and streamed straight into the archive.name: alldumps every MariaDB database. Delete the section for a database you don't run. Setting the databases up is in install PostgreSQL and install MariaDB.
Copy /etc/borgmatic/passphrase into your password manager now. If web1 is lost, so is that file, and the backups can't be decrypted without it.
Let root dump PostgreSQL
MariaDB needs nothing: on Ubuntu, root logs in through the Unix socket. PostgreSQL only lets the postgres Linux user in as the postgres role, and borgmatic runs as root. Running sudo -u postgres pg_dump doesn't work under the systemd service either, because the service is locked down with NoNewPrivileges and sudo fails with sudo: PERM_SUDOERS: setresuid(-1, 1, -1): Operation not permitted.
Instead, map the root user to the postgres role for socket logins. In /etc/postgresql/16/main/pg_hba.conf (use your version number), add map=backup to the first line:
local all postgres peer map=backupAt the end of /etc/postgresql/16/main/pg_ident.conf, allow both root and postgres to use that role:
backup root postgres
backup postgres postgresReload PostgreSQL and check:
systemctl reload postgresql
psql -U postgres -c 'SELECT current_user;'It prints postgres without asking for a password. Nothing changes for logins from the network.
Create the repository and the first backup
On web1:
borgmatic init --encryption repokey-blake2repokey-blake2 keeps the encrypted key inside the repository, protected by your passphrase. Export a copy of the key as well, and store it with the passphrase in your password manager; if the repository's copy is ever damaged, this is the only other one:
borgmatic key exportNow run a backup and watch it:
borgmatic create --verbosity 1 --statsThe end of the output shows the archive's size before and after compression and deduplication. Run plain borgmatic (no action) to do everything a scheduled run does: create, prune, compact and the checks that are due.
Check it worked
borgmatic list
borgmatic list --archive latest --find etc/hostnameThe first lists your archives, named after the host and time (web1-2026-10-21T23:53:23.463655). The second shows the file inside the newest one.
Run it every day
The Ubuntu package ships a systemd timer that runs borgmatic once a day at a random time (up to three hours after midnight), and catches up on a missed run after a reboot. Turn it on:
systemctl enable --now borgmatic.timer
systemctl list-timers borgmatic.timerThe service runs with a low CPU and disk priority and blocks shutdown while a backup is in progress. To see the last run:
journalctl -u borgmatic.service -n 30 --no-pagerTo pick your own time, run systemctl edit borgmatic.timer and add:
[Timer]
OnCalendar=
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15mThe empty OnCalendar= clears the packaged schedule before setting yours.
Restore files and databases
Try this now, not on the day you need it.
Restore one directory from the newest archive into a scratch location:
mkdir -p /tmp/restore
borgmatic extract --archive latest --path var/www/site --destination /tmp/restore
ls -R /tmp/restorePaths inside an archive have no leading /. Use an archive name from borgmatic list instead of latest to go back further.
Restore the databases from the newest archive. This overwrites the current contents of every database in the configuration:
borgmatic restore --archive latestAdd --database appdb to restore just one.
Clean up the space on the backup server
Because web1's key is append-only, the nightly prune and compact only mark old archives as deleted. The space comes back when you compact the repository on the backup server, where the append-only rule doesn't apply. Do it every month or two, after checking from web1 that the archives you expect are still listed (borgmatic list). If an attacker had pruned everything through web1's key, compacting would make that permanent; checking first is what makes append-only mode worth having.
On the backup server:
sudo -u borg borg compact --verbose /home/borg/repos/web1compaction freed about 22.56 kB repository space.Compacting doesn't need the passphrase: the backup server never sees your data unencrypted. /home/borg/repos/web1/transactions logs every change with its time, which tells you what happened if you ever need to roll back; Borg's append-only documentation explains how.
Troubleshooting
Remote: borg@backup.example.com: Permission denied (publickey,password). The backup server didn't accept the key. Check that the public key in authorized_keys is complete and on one line, that the file belongs to borg with mode 600, and that ssh_command points at the right private key.
Remote: Host key verification failed. web1 doesn't know the backup server's host key. Run the ssh-keyscan step again (and compare the fingerprint).
Repository path not allowed: /home/borg/repos/.... The path in repositories doesn't match --restrict-to-path in authorized_keys. Remember the ./ in the URL means "inside the borg user's home".
pg_dump: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: FATAL: role "root" does not exist. The PostgreSQL entry has no username: postgres, so pg_dump tried a role named after the Linux user. Add it, and the pg_ident.conf mapping above.
Next steps
- Prefer object storage to a second server? Back up with restic to S3-compatible storage.
- Get told when a backup fails: borgmatic can ping a monitoring service on start, finish and failure; see monitoring hooks in its documentation.
- Lock down SSH on the backup server too: SSH keys only, and a firewall that allows SSH from your servers.