--- title: "How to Back Up Ubuntu 24.04 with BorgBackup over SSH" description: "Set up encrypted, deduplicated BorgBackup backups from Ubuntu 24.04 to a remote SSH server, automate them with a systemd timer, prune old archives and restore." category: "Backup Recovery" language: en source: https://cubepath.com/docs/backup-recovery/backup-with-borgbackup-deduplication updated: 2026-09-25 --- # How to Back Up Ubuntu 24.04 with BorgBackup over SSH BorgBackup (Borg) is a deduplicating backup program: it splits files into chunks, stores each unique chunk only once, compresses it and encrypts it on the client before it leaves the server. Nightly backups of a server with little change take seconds and a few megabytes, while every archive can still be restored as a full copy. In this tutorial you will back up an Ubuntu 24.04 server to a Borg repository on a second server over SSH, automate it with a systemd timer, apply a retention policy and restore files. ## Prerequisites To follow this guide you need: - A **client server** running Ubuntu 24.04 LTS (for example a CubePath VPS) with a non-root user with `sudo` privileges. This is the server you want to back up. - A **backup server** running Ubuntu 24.04 or Debian 12 with SSH access, a `sudo` user and enough disk space, ideally in a different data center. Its address is referred to as `your_backup_server_ip`. - A password manager or other safe place outside both servers to store the repository key and passphrase. ## Step 1 - Installing BorgBackup on both servers Borg must be installed on the client and on the backup server, because the client talks to a `borg serve` process on the remote side. Run this on **both** servers: ```bash sudo apt update sudo apt install borgbackup ``` Check the version: ```bash borg --version ``` ```output borg 1.2.8 ``` Ubuntu 24.04 ships Borg 1.2. Keep both servers on the same major version (1.x); Borg 2 uses a different, incompatible repository format. ## Step 2 - Preparing the repository user on the backup server On the **backup server**, create a dedicated user whose home directory holds the repositories: ```bash sudo useradd --create-home --home-dir /srv/borg --shell /bin/bash borg sudo chmod 700 /srv/borg sudo -u borg mkdir -m 700 /srv/borg/.ssh ``` `useradd` leaves the account without a password, so it can only log in with an SSH key. ## Step 3 - Creating an SSH key restricted to Borg The backup will run as root on the **client server** so it can read every file. Create a dedicated key for it without a passphrase, so it can run unattended: ```bash sudo ssh-keygen -t ed25519 -f /root/.ssh/borg_ed25519 -N "" -C "borg@$(hostname -s)" sudo cat /root/.ssh/borg_ed25519.pub ``` On the **backup server**, open the `authorized_keys` file of the `borg` user: ```bash sudo -u borg nano /srv/borg/.ssh/authorized_keys ``` Add the public key on a single line, prefixed with a forced command. This key can then only run `borg serve`, and only inside `/srv/borg/web01`: ```text command="borg serve --restrict-to-path /srv/borg/web01",restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... borg@web01 ``` Replace `web01` with a name for the client, and use one line with its own path for every client you add later. Then fix the permissions: ```bash sudo chmod 600 /srv/borg/.ssh/authorized_keys ``` > Tip: Adding `--append-only` after `borg serve` prevents this client from deleting or overwriting existing data, which protects your history if the client server is compromised. Pruning must then be done from the backup server itself. Start without it and add it once the setup works. ## Step 4 - Initializing an encrypted repository The rest of the commands on the client run as root. Open a root shell so the environment variables below apply to every command: ```bash sudo -i ``` Tell Borg where the repository is and which SSH key to use: ```bash export BORG_REPO='ssh://borg@your_backup_server_ip/srv/borg/web01' export BORG_RSH='ssh -i /root/.ssh/borg_ed25519' ``` Accept the backup server's host key once: ```bash ssh -i /root/.ssh/borg_ed25519 borg@your_backup_server_ip ``` Answer `yes` to the fingerprint prompt. Because of the forced command, the session does not open a shell; press `Ctrl+C` to exit. Now create the repository with the `repokey-blake2` mode, which encrypts all data with a key stored inside the repository and protected by your passphrase: ```bash borg init --encryption=repokey-blake2 ``` Enter a long passphrase twice. Borg creates the `/srv/borg/web01` directory on the backup server and prints a reminder that you need both the key and the passphrase to access the repository. Export the key and store it, together with the passphrase, somewhere outside both servers. If the repository's key is ever damaged, this exported copy is the only way to read your backups: ```bash borg key export "$BORG_REPO" /root/borg-web01.key cat /root/borg-web01.key ``` Copy the output into your password manager, then delete the local file: ```bash rm /root/borg-web01.key ``` For unattended runs, store the passphrase in a file only root can read: ```bash install -m 600 /dev/null /root/.borg-passphrase nano /root/.borg-passphrase ``` Type the passphrase on the first line and save. Then tell Borg to read it from there: ```bash export BORG_PASSCOMMAND='cat /root/.borg-passphrase' ``` ## Step 5 - Creating your first backup `borg create` takes an archive name and a list of paths. The `::` prefix means "the repository in `BORG_REPO`", and `{hostname}` and `{now}` are placeholders Borg fills in: ```bash borg create --stats --compression zstd,3 --exclude-caches \ --exclude 'sh:home/*/.cache' \ --exclude 'sh:root/.cache' \ ::'{hostname}-{now:%Y-%m-%dT%H:%M}' \ /etc /home /root /var/www ``` The options do the following: - `--compression zstd,3`: fast compression with a good ratio. Use `lz4` for the lowest CPU usage. - `--exclude-caches`: skip directories marked with a standard `CACHEDIR.TAG` file. - `--exclude 'sh:...'`: shell-style patterns. Borg stores paths without the leading slash, so patterns start with `home/`, not `/home/`. The statistics at the end look like this: ```output ------------------------------------------------------------------------------ Repository: ssh://borg@your_backup_server_ip/srv/borg/web01 Archive name: web01-2026-09-25T10:20 Time (start): Thu, 2026-09-25 10:20:03 Time (end): Thu, 2026-09-25 10:21:47 Duration: 1 minutes 44.12 seconds Number of files: 48213 ------------------------------------------------------------------------------ Original size Compressed size Deduplicated size This archive: 2.14 GB 964.20 MB 912.51 MB All archives: 2.14 GB 964.20 MB 912.51 MB ------------------------------------------------------------------------------ ``` Run the same command again. The second archive finishes much faster, and its **Deduplicated size** is only a few megabytes, because almost every chunk already exists in the repository. If the server runs a database, dump it before the backup (for example with `mysqldump --single-transaction`) into a directory that is part of the backup. Borg then deduplicates the dumps too. ## Step 6 - Listing and restoring archives List the archives in the repository: ```bash borg list ``` ```output web01-2026-09-25T10:20 Thu, 2026-09-25 10:20:03 [3c9e1f0a...] web01-2026-09-25T10:24 Thu, 2026-09-25 10:24:11 [8b2d77e4...] ``` List the contents of one archive, filtered to a path: ```bash borg list ::web01-2026-09-25T10:24 etc/nginx ``` `borg extract` restores into the **current directory**, recreating the full path. Always restore into an empty directory first: ```bash mkdir -p /tmp/borg-restore && cd /tmp/borg-restore borg extract ::web01-2026-09-25T10:24 etc/nginx diff -r /etc/nginx /tmp/borg-restore/etc/nginx && echo "identical" ``` ```output identical ``` Ownership and permissions are restored as well, because you ran the command as root. Once you have checked the files, copy what you need back into place. ## Step 7 - Pruning old archives Without a retention policy, the repository keeps every archive forever. `borg prune` deletes archives that fall outside the policy, and `borg compact` (required since Borg 1.2) frees the disk space they used. Preview what would be removed: ```bash borg prune --list --dry-run --glob-archives '{hostname}-*' \ --keep-daily 7 --keep-weekly 4 --keep-monthly 6 ``` This keeps the last 7 daily, 4 weekly and 6 monthly archives. `--glob-archives '{hostname}-*'` limits pruning to this server's archives. When the list looks right, run it without `--dry-run` and compact: ```bash borg prune --list --glob-archives '{hostname}-*' \ --keep-daily 7 --keep-weekly 4 --keep-monthly 6 borg compact ``` ## Step 8 - Automating backups with a systemd timer Put the steps into a script. It treats Borg's exit code 1 (warnings, for example a file that changed while being read) as success, and fails on anything higher: ```bash nano /usr/local/sbin/borg-backup ``` ```bash #!/usr/bin/env bash set -euo pipefail export BORG_REPO='ssh://borg@your_backup_server_ip/srv/borg/web01' export BORG_RSH='ssh -i /root/.ssh/borg_ed25519 -o BatchMode=yes' export BORG_PASSCOMMAND='cat /root/.borg-passphrase' rc=0 borg create --stats --compression zstd,3 --exclude-caches \ --exclude 'sh:home/*/.cache' \ --exclude 'sh:root/.cache' \ ::'{hostname}-{now:%Y-%m-%dT%H:%M}' \ /etc /home /root /var/www || rc=$? if [ "$rc" -gt 1 ]; then echo "borg create failed with exit code $rc" >&2 exit "$rc" fi borg prune --list --glob-archives '{hostname}-*' \ --keep-daily 7 --keep-weekly 4 --keep-monthly 6 borg compact ``` If you enabled `--append-only` in Step 3, remove the `prune` and `compact` lines: those operations must then run on the backup server. Make the script executable by root only: ```bash chmod 700 /usr/local/sbin/borg-backup ``` Create the service unit: ```bash nano /etc/systemd/system/borg-backup.service ``` ```ini [Unit] Description=BorgBackup to the backup server Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/borg-backup Nice=10 IOSchedulingClass=idle ``` And the timer: ```bash nano /etc/systemd/system/borg-backup.timer ``` ```ini [Unit] Description=Nightly BorgBackup [Timer] OnCalendar=*-*-* 02:30:00 RandomizedDelaySec=20m Persistent=true [Install] WantedBy=timers.target ``` Enable the timer, run the service once and check the result: ```bash systemctl daemon-reload systemctl enable --now borg-backup.timer systemctl start borg-backup.service journalctl -u borg-backup.service -n 30 --no-pager ``` The journal shows the `--stats` block and the list of pruned archives, ending with `Finished borg-backup.service`. `systemctl list-timers borg-backup.timer` shows the next scheduled run. ## Step 9 - Verifying repository integrity `borg check` verifies the repository structure and archive metadata. Run it now and then monthly: ```bash borg check ``` It prints nothing and exits with code 0 when everything is consistent. `borg check --verify-data` also decrypts and verifies every chunk, which reads the whole repository and takes much longer; run it occasionally, outside backup hours. Exit the root shell when you are done: ```bash exit ``` ## Troubleshooting - **`Repository path not allowed`**: the path in `BORG_REPO` does not match the `--restrict-to-path` value in `authorized_keys`. - **`Failed to create/acquire the lock`**: another Borg process is running, or a previous run was killed. If you are sure nothing is running, remove the stale lock with `borg break-lock`. - **`Cache is newer than repository`** or a warning that the repository was relocated: the repository was changed by another client or moved. Borg asks for confirmation interactively; run it once by hand from a root shell and answer the prompt. - **`passphrase supplied in BORG_PASSPHRASE, by BORG_PASSCOMMAND or via BORG_PASSPHRASE_FD is incorrect`**: the file contains a different passphrase or a trailing character. Recreate it with `nano` and try `borg list` by hand. ## Conclusion Your server now sends encrypted, compressed and deduplicated backups to a second server every night, keeps a predictable history and can restore any file from any archive. The key export in your password manager makes the backups recoverable even if both servers are lost. As next steps, enable `--append-only` and prune from the backup server, add an offsite copy to object storage with [restic](/docs/backup-recovery/backup-with-restic), and review [the 3-2-1 backup rule](/docs/backup-recovery/backup-strategies-3-2-1-and-best-practices) to schedule regular restore tests.