
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 pruneand 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/wwwor/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/wwwor/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.phpexist 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 duplicateduploads/trees. - SSH works but Borg fails: re-check
ForceCommand borg serveand confirm the key landed in theborguser’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
zstdlevel, keepNiceand 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-dataon 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.