Back to tutorials
Tutorial

BorgBackup Setup Guide Tutorial (2026): Encrypted Offsite Backups for a Hosting VPS (Retention, Prune, Restore Test)

BorgBackup setup guide tutorial (2026) for encrypted offsite VPS backups with retention, prune rules, and real restore testing.

By Anurag Singh
Updated on Jun 24, 2026
Category: Tutorial
Share article
BorgBackup Setup Guide Tutorial (2026): Encrypted Offsite Backups for a Hosting VPS (Retention, Prune, Restore Test)

Your backups aren’t “working” until you’ve restored from them at least once. This BorgBackup setup guide tutorial walks you through encrypted, deduplicated offsite backups for a hosting VPS.

Then you’ll prove the restore path while it’s still a calm day.

The examples use Ubuntu 24.04 LTS. The same flow applies to Debian 12/13 and most RHEL-family distributions, with small tweaks.

You’ll create a repository on a separate server, lock it down, schedule backups, set retention, and run a restore drill you can repeat later.

What you’ll build in this BorgBackup setup guide tutorial

  • An offsite Borg repository on a separate “backup VPS” (recommended) or dedicated server
  • Encrypted backups with repokey-blake2 (fast, secure defaults)
  • Compression tuned for web hosting workloads
  • Retention policy with borg prune and sanity checks
  • A real restore test for one WordPress site (files + database)

If you don’t already have a second host for offsite storage, spin up a small HostMyCode VPS as a dedicated backup target.

Keeping backups on the same machine you’re trying to recover is how “we have backups” turns into “we had backups.”

Prerequisites and layout (source server vs backup server)

You’ll use two machines:

  • Source VPS: where your websites and email live (the machine you’re backing up)
  • Backup server: a separate VPS/dedicated server that stores the Borg repository

Assumptions in this tutorial:

  • You can SSH as a sudo user on both servers
  • SSH runs on port 22 (adjust if yours differs)
  • You can open firewall access from the source VPS to the backup server
  • Your web root is in something like /var/www or /home/*/public_html (adjust paths)

Before you automate anything, confirm SSH and firewall rules are stable.

If keys, users, and rules are still changing, fix that first.

This pairs well with SSH key setup best practices and a safe UFW lockdown for hosting servers.

Step 1: Prepare the backup server (dedicated user + locked-down SSH)

On the backup server, create a dedicated user that will own the Borg repository:

sudo adduser --disabled-password --gecos "" borg
sudo mkdir -p /srv/borg
sudo chown borg:borg /srv/borg
sudo chmod 700 /srv/borg

Install Borg:

sudo apt update
sudo apt install -y borgbackup

Next, restrict SSH for that user. Borg connects over SSH.

Treat this account as “backup transport only,” not a general login.

Edit /etc/ssh/sshd_config (or drop a file into /etc/ssh/sshd_config.d/ on Ubuntu):

sudo nano /etc/ssh/sshd_config.d/50-borg.conf

Add:

Match User borg
  X11Forwarding no
  AllowTcpForwarding no
  PermitTTY no
  ForceCommand borg serve

Reload SSH:

sudo systemctl reload ssh

Firewall note: only allow SSH to the backup server from your source VPS IP (and your admin IP). If you’re using UFW:

sudo ufw allow from <SOURCE_VPS_IP> to any port 22 proto tcp
sudo ufw allow from <YOUR_ADMIN_IP> to any port 22 proto tcp
sudo ufw enable

Step 2: Set up SSH keys from the source VPS to the backup server

On the source VPS, generate a dedicated keypair for backups.

Keep it separate from your personal admin key. This makes rotations easier and limits collateral damage if you need to replace a key.

sudo -i
mkdir -p /root/.ssh
chmod 700 /root/.ssh
ssh-keygen -t ed25519 -a 64 -f /root/.ssh/borg_ed25519 -C "borg-backup"

Copy the public key to the backup server’s borg user:

ssh-copy-id -i /root/.ssh/borg_ed25519.pub borg@<BACKUP_SERVER_IP>

Test SSH connectivity:

ssh -i /root/.ssh/borg_ed25519 borg@<BACKUP_SERVER_IP> borg --version

If your ForceCommand borg serve is active, the test should still work.

If it doesn’t, check /var/log/auth.log on the backup server for the exact refusal.

Step 3: Initialize the Borg repository (encrypted)

Still on the source VPS, point Borg at the remote repository location.

Use a naming scheme you won’t regret later. This matters even more once you add a second or third source server.

export BORG_REPO='borg@<BACKUP_SERVER_IP>:/srv/borg/source-vps-01'
export BORG_RSH='ssh -i /root/.ssh/borg_ed25519'

Initialize with encryption:

borg init --encryption=repokey-blake2 "$BORG_REPO"

Borg will prompt for a passphrase. Store it in a password manager, not on paper.

Next, export the repo key and store it off the server. This is what saves you if the source VPS dies:

mkdir -p /root/borg-key-backup
chmod 700 /root/borg-key-backup
borg key export "$BORG_REPO" /root/borg-key-backup/source-vps-01.borg.key

Move that exported key to secure offline storage.

If you lose both the passphrase and the key, the data is unrecoverable by design.

Step 4: Decide what to back up (and what to exclude)

On a typical hosting VPS, you usually want:

  • Website files (e.g., /var/www or /home)
  • Web server config (Nginx/Apache virtual hosts)
  • SSL material (Let’s Encrypt configs, not necessarily private keys if you can re-issue)
  • Crons, system config needed to rebuild the service

You usually don’t want:

  • Cache directories (*/cache, page cache, temp files)
  • Large logs that rotate anyway
  • Docker image layers, node_modules, vendor caches (unless needed)
  • Database data directories (/var/lib/mysql) — back up databases via dumps unless you’re doing consistent snapshots

Create an exclude file on the source VPS:

nano /root/borg-excludes.txt

Example excludes (tune for your stack):

# OS and runtime noise
/proc
/sys
/dev
/run
/tmp
/var/tmp

# Package caches
/var/cache

# Logs (you can keep selected logs if you want)
/var/log

# Common web/app caches
**/cache
**/tmp
**/.cache

# WordPress cache plugins (examples)
**/wp-content/cache
**/wp-content/w3tc-cache
**/wp-content/uploads/cache

If the server already has disk bloat, clean it up before the first run.

Your first archive becomes the baseline. You don’t want to bake old junk into every retention window.

This pairs well with a safe VPS cleanup workflow.

Step 5: Run your first backup (with sane compression and stats)

Install Borg on the source VPS if needed:

sudo apt update
sudo apt install -y borgbackup

Run the first archive and name it with a timestamp:

export BORG_REPO='borg@<BACKUP_SERVER_IP>:/srv/borg/source-vps-01'
export BORG_RSH='ssh -i /root/.ssh/borg_ed25519'
export BORG_PASSPHRASE='<SET_THIS_IN_YOUR_ENV_OR_USE_BORG_PASSCOMMAND>'

borg create \
  --verbose --stats --progress \
  --compression zstd,6 \
  --exclude-caches \
  --exclude-from /root/borg-excludes.txt \
  "$BORG_REPO"::"{hostname}-{now:%Y-%m-%d_%H%M}" \
  /etc \
  /home \
  /var/www \
  /var/spool/cron

About compression: zstd,6 is a sensible default for mixed hosting data (PHP, images, configs).

If CPU is tight, drop to zstd,3.

If you have headroom and want smaller archives, test 10–12 and watch your backup window.

List archives:

borg list "$BORG_REPO"

Step 6: Add retention with prune (daily/weekly/monthly)

Retention keeps storage predictable. It also stops your backup repo from growing without limits.

A practical baseline for small-to-medium hosting looks like this:

  • Keep 7 daily
  • Keep 4 weekly
  • Keep 6 monthly

Run a dry-run prune first so you can see what would be removed:

borg prune \
  --list --dry-run \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  "$BORG_REPO"

If the selection looks right, run the real prune.

Then compact segments (Borg 1.2/1.4 uses borg compact):

borg prune \
  --list \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  "$BORG_REPO"

borg compact "$BORG_REPO"

Finally, check repository health:

borg check --verify-data "$BORG_REPO"

Run --verify-data less often (weekly/monthly). It’s I/O heavy.

For daily jobs, either skip verification or use a lighter cadence. Then rely on scheduled deep checks.

Step 7: Automate with a script + systemd timer (recommended)

Cron is fine. On VPSes, systemd timers often work better.

You get cleaner logs and easier troubleshooting. You also get more predictable behavior after reboots.

Create a script on the source VPS:

nano /usr/local/sbin/borg-backup.sh

Example script:

#!/usr/bin/env bash
set -euo pipefail

export BORG_REPO='borg@<BACKUP_SERVER_IP>:/srv/borg/source-vps-01'
export BORG_RSH='ssh -i /root/.ssh/borg_ed25519'
export BORG_PASSPHRASE='<STORE_IN_A_SAFE_SECRET_MANAGER_IF_POSSIBLE>'

ARCHIVE="$(hostname)-$(date +%F_%H%M)"

borg create \
  --stats \
  --compression zstd,6 \
  --exclude-caches \
  --exclude-from /root/borg-excludes.txt \
  "$BORG_REPO"::"$ARCHIVE" \
  /etc /home /var/www /var/spool/cron

borg prune \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  "$BORG_REPO"

borg compact "$BORG_REPO"

Make it executable:

chmod 700 /usr/local/sbin/borg-backup.sh

Create a systemd service:

nano /etc/systemd/system/borg-backup.service
[Unit]
Description=BorgBackup nightly offsite backup
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/borg-backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

Create a timer:

nano /etc/systemd/system/borg-backup.timer
[Unit]
Description=Run BorgBackup nightly

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=600

[Install]
WantedBy=timers.target

Enable and start:

systemctl daemon-reload
systemctl enable --now borg-backup.timer
systemctl list-timers --all | grep borg

Check logs after the first run:

journalctl -u borg-backup.service -n 200 --no-pager

Security note: putting BORG_PASSPHRASE in a script is easy. It’s also a liability.

A safer pattern is BORG_PASSCOMMAND reading from a root-only file or a secrets tool.

Either way, keep secrets out of shell history. Also make sure you’re not backing up the secret store into the same repo.

Step 8: Back up databases correctly (example: WordPress via mysqldump)

File backups alone won’t restore a dynamic site.

For WordPress on MySQL/MariaDB, take a database dump before the Borg archive. Then include that dump in the backup.

Create a directory for DB dumps:

mkdir -p /root/db-dumps
chmod 700 /root/db-dumps

Dump one database (replace credentials):

mysqldump --single-transaction --quick --routines --triggers \
  -u wp_user -p'YOUR_PASSWORD' wp_database \
  | gzip -9 > /root/db-dumps/wp_database.sql.gz

Then ensure /root/db-dumps is included in your Borg paths, or add it explicitly to borg create.

If you manage multiple accounts, dump per site and keep filenames stable.

Stable names make failures easier to spot.

If you’re running cPanel/WHM, you may prefer cPanel’s own backup system for account-level restores.

For cPanel hardening alongside backups, keep this nearby: cPanel security checklist.

Step 9: Run a restore test (the part most people skip)

A restore test is where the truth comes out.

You’ll catch missing paths, over-aggressive excludes, permissions you can’t reapply, or a database dump that quietly failed weeks ago.

Pick one site. Restore a small set of files to a temporary location on the source VPS (or, better, a staging VPS):

export BORG_REPO='borg@<BACKUP_SERVER_IP>:/srv/borg/source-vps-01'
export BORG_RSH='ssh -i /root/.ssh/borg_ed25519'

borg list "$BORG_REPO"

mkdir -p /root/restore-test
borg extract "$BORG_REPO"::"source-vps-01-2026-06-24_0230" \
  --target /root/restore-test \
  home/exampleuser/public_html/wp-config.php \
  root/db-dumps/wp_database.sql.gz

Now validate:

  • Does wp-config.php exist and look correct?
  • Is the dump file present and non-empty?
  • Can you decompress the dump?
gzip -t /root/restore-test/root/db-dumps/wp_database.sql.gz
ls -lh /root/restore-test/root/db-dumps/wp_database.sql.gz

If you want full proof, restore the site into a new vhost and import into a test database.

If you’re planning for real disaster recovery, practice the DNS part too.

HostMyCode has a solid cutover walkthrough: low-downtime DNS cutover with TTL planning and rollback.

Quick diagnostics: common BorgBackup problems on hosting VPS

  • “Permission denied” on extract: you restored without root, or you backed up files with restrictive perms. Run restores as root when you need to preserve ownership and modes.
  • Backups are huge: your exclude list missed caches or you’re backing up backups. Hunt for nested backup/, cache/, and duplicated uploads/ trees.
  • SSH works but Borg fails: re-check ForceCommand borg serve and confirm the key landed in the borg user’s authorized keys.
  • Prune didn’t free space: run borg compact. Prune chooses what to delete; compact actually reclaims space from segments.
  • CPU spikes at night: lower the zstd level, keep Nice and I/O scheduling (already in the systemd service), or move the backup window.

Operational checklist (printable)

  • Repository is offsite and reachable only from your source VPS IP
  • Repo is encrypted and the key is exported to offline storage
  • Excludes avoid caches/logs but keep critical configs and site files
  • Database dumps run before the archive and are included in backups
  • Retention is defined and prune + compact run automatically
  • You run a deep borg check --verify-data on a schedule
  • You perform a restore test at least quarterly (monthly is better)

Summary: a backup you can actually restore

Borg gives you encrypted, deduplicated backups that stay efficient as your VPS changes over time.

Reliability comes from routine, not tooling.

Exclude the right junk, keep database dumps consistent, and practice restores until they’re boring.

If you want backups on infrastructure you can trust, run production sites on a managed VPS hosting plan and place repositories on a separate HostMyCode VPS.

Two machines, one role each, and fewer surprises during an outage.

If you’re building offsite backups for client websites, start with a clean two-server setup: one box for production, one box dedicated to backups. HostMyCode keeps that simple with HostMyCode VPS plans for backup targets and managed VPS hosting when you’d rather not spend your weekends on patching and maintenance windows.

FAQ

Is BorgBackup safe for hosting client data?

Yes, if you enable encryption at repo init and protect the passphrase/key.

Also lock down the repo user’s SSH access and restrict inbound SSH by IP.

Should I back up /var/lib/mysql with Borg?

Not for typical hosting backups. Use consistent logical dumps (or purpose-built hot backup tooling) to avoid an inconsistent database state.

How often should I run borg check --verify-data?

Weekly to monthly is common.

Schedule it during low traffic because it can be I/O heavy, especially on large repositories.

What’s a good retention policy for small business hosting?

7 daily, 4 weekly, and 6 monthly is a practical baseline.

If you deploy frequently, add hourly for a short window (for example, keep 24 hourly).

Can I use Borg with cPanel or DirectAdmin?

You can, but consider account-level backups through the control panel if you need one-click restores per customer.

Borg works well as an additional offsite layer or for full-server recovery workflows.