# 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.

Source: https://vpsnine.com/tutorials/borgmatic-backup-setup · Updated: 2026-10-21

Difficulty: intermediate · Time: about 40 minutes · Tested on: Ubuntu 24.04 LTS

Prerequisites:

- 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](https://vpsnine.com/help/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:**

```bash
sudo apt update
sudo apt install -y borgbackup
```

**On web1** also install borgmatic:

```bash
sudo apt install -y borgmatic
borgmatic --version
borg --version
```

Ubuntu 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:

```bash
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:

```bash
ssh-keygen -t ed25519 -N '' -C 'borg@web1' -f /root/.ssh/borg_ed25519
cat /root/.ssh/borg_ed25519.pub
```

**On the backup server**, add that public key to `/home/borg/.ssh/authorized_keys` on one line, with the restrictions in front of it:

```text
command="borg serve --append-only --restrict-to-path /home/borg/repos/web1",restrict ssh-ed25519 AAAAC3... borg@web1
```

```bash
sudo chown borg:borg /home/borg/.ssh/authorized_keys
sudo chmod 600 /home/borg/.ssh/authorized_keys
```

What 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-path` limits it to web1's own repository. Asking for another one fails with `Repository path not allowed`.
- `--append-only` means 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).
- `restrict` turns 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:

```bash
ssh-keyscan -t ed25519 backup.example.com >> /root/.ssh/known_hosts
```

Compare the fingerprint with the one the backup server shows for `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub`.

### Check it worked

```bash
ssh -i /root/.ssh/borg_ed25519 borg@backup.example.com ls
```

Instead 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:

```bash
mkdir -p /etc/borgmatic
chmod 700 /etc/borgmatic
openssl rand -base64 32 > /etc/borgmatic/passphrase
chmod 600 /etc/borgmatic/passphrase
```

Then `/etc/borgmatic/config.yaml`:

```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: all
```

```bash
chmod 600 /etc/borgmatic/config.yaml
borgmatic config validate
```

The last command prints `All configuration files are valid`. What the settings do:

- **`source_directories`**: what you made, not the operating system. Add `/srv`, `/opt/yourapp` or Docker volumes if your apps keep data there.
- **`path: ssh://borg@backup.example.com/./repos/web1`**: the `./` makes the path relative to the `borg` user'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 for `prune`: 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_databases` and `mariadb_databases`**: dumped at backup time and streamed straight into the archive. `name: all` dumps every MariaDB database. Delete the section for a database you don't run. Setting the databases up is in [install PostgreSQL](https://vpsnine.com/tutorials/install-postgresql-ubuntu) and [install MariaDB](https://vpsnine.com/tutorials/install-mariadb-ubuntu).

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:

```text
local   all             postgres                                peer map=backup
```

At the end of `/etc/postgresql/16/main/pg_ident.conf`, allow both root and postgres to use that role:

```text
backup          root                    postgres
backup          postgres                postgres
```

Reload PostgreSQL and check:

```bash
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:**

```bash
borgmatic init --encryption repokey-blake2
```

`repokey-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:

```bash
borgmatic key export
```

Now run a backup and watch it:

```bash
borgmatic create --verbosity 1 --stats
```

The 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

```bash
borgmatic list
borgmatic list --archive latest --find etc/hostname
```

The 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:

```bash
systemctl enable --now borgmatic.timer
systemctl list-timers borgmatic.timer
```

The service runs with a low CPU and disk priority and blocks shutdown while a backup is in progress. To see the last run:

```bash
journalctl -u borgmatic.service -n 30 --no-pager
```

To pick your own time, run `systemctl edit borgmatic.timer` and add:

```ini
[Timer]
OnCalendar=
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15m
```

The 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:

```bash
mkdir -p /tmp/restore
borgmatic extract --archive latest --path var/www/site --destination /tmp/restore
ls -R /tmp/restore
```

Paths 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:

```bash
borgmatic restore --archive latest
```

Add `--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:**

```bash
sudo -u borg borg compact --verbose /home/borg/repos/web1
```

```text
compaction 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](https://borgbackup.readthedocs.io/en/stable/usage/notes.html#append-only-mode-forbid-compaction) 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](https://vpsnine.com/tutorials/restic-backup-s3).
- Get told when a backup fails: borgmatic can ping a monitoring service on start, finish and failure; see [monitoring hooks](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/) in its documentation.
- Lock down SSH on the backup server too: [SSH keys](https://vpsnine.com/help/ssh-keys) only, and a [firewall](https://vpsnine.com/help/firewall-with-ufw) that allows SSH from your servers.
