Backup Strategies for Self-Hosted Servers That Work

by David Park
Backup Strategies for Self-Hosted Servers That Work

Backup Strategies for Self-Hosted Servers That Work

By the end of this guide you will have three independent backup layers running on Ubuntu 24.04: local filesystem snapshots with restic, encrypted offsite replication to Backblaze B2, and provider-level block-storage snapshots via the Hetzner API. You will also have a cron-driven restore test so you know the backups actually work before you need them. Prerequisites: a running Ubuntu 24.04 VPS (any provider), a Backblaze B2 account (free tier covers the setup), a Hetzner Cloud account if you want provider snapshots, and root or sudo access.


Why Three Layers?

A single backup location is not a backup strategy — it is a false sense of security. The 3-2-1 rule is the minimum viable standard: 3 copies of data, on 2 different media types, with 1 copy offsite. For a self-hosted server that translates to:

  • Layer 1 — Local restic repository on an attached volume: fast restores for accidental deletes.
  • Layer 2 — Encrypted remote repository on Backblaze B2: survives datacenter fire, provider outage, or account compromise.
  • Layer 3 — Provider block-storage snapshot: full-disk point-in-time image you can boot from if the OS is corrupted.

Each layer has a different RTO (recovery time objective). Layer 1 restores a single file in seconds. Layer 3 restores the whole server in under five minutes.


Prerequisites

  • Ubuntu 24.04 LTS (fresh or existing server)
  • sudo privileges
  • Backblaze B2 bucket name, Application Key ID, and Application Key
  • (Optional) Hetzner Cloud API token with Read/Write scope
  • Outbound HTTPS (port 443) allowed in your firewall

Step 1 — Install restic

restic is a single binary with no daemon. Install the distro package, then pin to the exact binary path used throughout this guide.

sudo apt update && sudo apt install -y restic
restic version

Expected output:

restic 0.16.4 compiled with go1.22.2 on linux/amd64

If the version is older than 0.16, install the upstream binary instead:

wget -q https://github.com/restic/restic/releases/download/v0.16.4/restic_0.16.4_linux_amd64.bz2
bunzip2 restic_0.16.4_linux_amd64.bz2
chmod +x restic_0.16.4_linux_amd64
sudo mv restic_0.16.4_linux_amd64 /usr/local/bin/restic

Step 2 — Create a Local Repository (Layer 1)

Attach a second volume in your provider dashboard (Hetzner: minimum 10 GB, ~$0.52/month). Mount it at /mnt/backups, then initialize a restic repository there.

2a. Format and mount the volume (replace /dev/sdb with your actual device):

sudo mkfs.ext4 /dev/sdb
sudo mkdir -p /mnt/backups
sudo mount /dev/sdb /mnt/backups
echo '/dev/sdb /mnt/backups ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab

2b. Store the repository password in a root-only file:

sudo bash -c 'echo "CHANGE_ME_STRONG_PASSPHRASE" > /etc/restic-local.pass'
sudo chmod 600 /etc/restic-local.pass

2c. Initialize the local repository:

sudo restic init \
  --repo /mnt/backups/local \
  --password-file /etc/restic-local.pass

Expected output:

created restic repository abc123def4 at /mnt/backups/local

Step 3 — Create an Offsite Repository on Backblaze B2 (Layer 2)

restic speaks the B2 API natively. Export your credentials as environment variables, then initialize a second repository.

3a. Store B2 credentials in a sourced environment file:

sudo bash -c 'cat > /etc/restic-b2.env <<EOF
export B2_ACCOUNT_ID="YOUR_KEY_ID"
export B2_ACCOUNT_KEY="YOUR_APPLICATION_KEY"
export RESTIC_PASSWORD_FILE="/etc/restic-b2.pass"
export RESTIC_REPOSITORY="b2:YOUR_BUCKET_NAME:restic"
EOF'
sudo chmod 600 /etc/restic-b2.env

3b. Create a separate passphrase file for the B2 repo (use a different passphrase from Layer 1):

sudo bash -c 'echo "DIFFERENT_STRONG_PASSPHRASE" > /etc/restic-b2.pass'
sudo chmod 600 /etc/restic-b2.pass

3c. Initialize the remote repository:

sudo bash -c 'source /etc/restic-b2.env && restic init'

Expected output:

created restic repository 7f9e12ab34 at b2:YOUR_BUCKET_NAME:restic

Step 4 — Write the Backup Script

One script handles both repositories, enforces retention policy, and logs results.

sudo nano /usr/local/bin/server-backup.sh

Paste the following content:

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

LOG="/var/log/restic-backup.log"
DATE=$(date '+%Y-%m-%d %H:%M:%S')

echo "[$DATE] Starting backup" >> "$LOG"

# --- Layer 1: local ---
restic backup \
  --repo /mnt/backups/local \
  --password-file /etc/restic-local.pass \
  --exclude /proc \
  --exclude /sys \
  --exclude /dev \
  --exclude /run \
  --exclude /mnt \
  --exclude /tmp \
  --exclude /var/cache \
  / >> "$LOG" 2>&1

restic forget \
  --repo /mnt/backups/local \
  --password-file /etc/restic-local.pass \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 3 \
  --prune >> "$LOG" 2>&1

# --- Layer 2: Backblaze B2 ---
source /etc/restic-b2.env

restic backup \
  --exclude /proc \
  --exclude /sys \
  --exclude /dev \
  --exclude /run \
  --exclude /mnt \
  --exclude /tmp \
  --exclude /var/cache \
  / >> "$LOG" 2>&1

restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --prune >> "$LOG" 2>&1

echo "[$DATE] Backup complete" >> "$LOG"

Save and make it executable:

sudo chmod 700 /usr/local/bin/server-backup.sh

Step 5 — Schedule with Cron

Run the backup daily at 02:00 UTC. Root crontab avoids permission issues when reading system directories.

sudo crontab -e

Add this line:

0 2 * * * /usr/local/bin/server-backup.sh

To verify the cron entry was saved:

sudo crontab -l

Expected output:

0 2 * * * /usr/local/bin/server-backup.sh

Step 6 — Provider Snapshot via Hetzner API (Layer 3)

Provider snapshots capture the entire disk image. Hetzner charges €0.0119/GB/month for snapshots. For a 20 GB root disk, that is under $0.25/month for a daily snapshot retained for 7 days.

6a. Install hcloud CLI:

wget -q https://github.com/hetznercloud/cli/releases/download/v1.43.1/hcloud-linux-amd64.tar.gz
tar -xzf hcloud-linux-amd64.tar.gz
sudo mv hcloud /usr/local/bin/hcloud
hcloud version

6b. Configure with your API token:

hcloud context create production
# Paste your API token when prompted

6c. Get your server ID:

hcloud server list

Expected output:

ID        NAME         STATUS    IPV4
12345678  my-server    running   1.2.3.4

6d. Add a snapshot cron job (weekly, Sunday 03:00 UTC):

sudo crontab -e

Add:

0 3 * * 0 hcloud server create-image --type snapshot --description "weekly-$(date +\%Y\%m\%d)" 12345678

Replace 12345678 with your actual server ID.


Verify It Works

Run the backup script manually and check the log:

sudo /usr/local/bin/server-backup.sh
tail -50 /var/log/restic-backup.log

Expected log tail:

[2025-05-10 02:00:01] Starting backup
Files:        1842 new, 0 changed, 0 unmodified
...
[2025-05-10 02:04:33] Backup complete

List snapshots in the local repo:

sudo restic snapshots \
  --repo /mnt/backups/local \
  --password-file /etc/restic-local.pass

Restore a single file to confirm integrity:

sudo restic restore latest \
  --repo /mnt/backups/local \
  --password-file /etc/restic-local.pass \
  --include /etc/nginx/nginx.conf \
  --target /tmp/restore-test

ls -lh /tmp/restore-test/etc/nginx/nginx.conf

Expected output:

-rw-r--r-- 1 root root 1.5K May 10 01:55 /tmp/restore-test/etc/nginx/nginx.conf

Verify B2 snapshots:

sudo bash -c 'source /etc/restic-b2.env && restic snapshots'

Troubleshooting

Fatal: unable to open config file: ... on restic init The repository path does not exist or the volume is not mounted. Run mount | grep /mnt/backups to confirm the volume is attached.

b2: 401 Unauthorized Your B2 Application Key ID or Key is wrong, or the key lacks read/write permissions on the bucket. Regenerate the key in the B2 dashboard and update /etc/restic-b2.env.

Backup log shows permission denied on /root or /home The script must run as root. Confirm with sudo crontab -l — user crontabs will not have access to those directories.

restic forget runs but disk usage on B2 does not drop Forget marks snapshots for deletion; --prune actually removes the data. The script already includes --prune. If space is still not reclaimed, run restic prune manually with the repo and password flags.

Hetzner snapshot cron fires but no snapshot appears Confirm hcloud is in root's PATH inside cron. Add the full path: /usr/local/bin/hcloud server create-image ....

Restore test produces an empty directory The --include path must match the exact path stored in the snapshot. Run restic ls latest --repo ... --password-file ... to list stored paths and adjust --include accordingly.


Next Steps

Once the three layers are running, add these improvements to strengthen cara backup WordPress otomatis gratis for self-hosted servers:

  • Alert on backup failure. Pipe the script's exit code to a dead-man's-switch service (Healthchecks.io has a free tier) so you get an email if the cron job silently fails.
  • Database-consistent snapshots. For PostgreSQL or MySQL, flush and lock tables before the restic run using pg_dump or mysqldump to a file inside the backup path, rather than backing up live data files.
  • Encrypt the local volume. Use LUKS (cryptsetup luksFormat /dev/sdb) before formatting with ext4 so that a stolen disk does not expose plaintext data. restic also encrypts its repository format, giving you defense in depth.
  • Monthly restore drills. Spin up a throwaway Hetzner CX11 ($0.015/hour), restore the B2 repository to it, and verify your application starts. Delete the server after the test. Total cost: under $0.50 per drill.