Building a Fast, Encrypted & Private Backup Pipeline with restic, rest‑server, Hetzner Storage Box & Tailscale VPN
A step-by-step guide to building an encrypted, off-site backup pipeline with restic, rest-server, Hetzner Storage Box, and Tailscale VPN. Updated for restic 0.18 with append-only mode and Prometheus monitoring.
version key), added append-only mode and Prometheus monitoring, and tightened security recommendations. Looking for a UI-based approach instead? Check out Zerobyte: A Web UI for restic That Replaces Your Backup Scripts.A step‑by‑step guide to reproducing (and understanding) the exact setup I use to keep my Proxmox homelab and personal data safe—updated for 2026.
1. Why this stack?
| Layer | What it does | Why I chose it |
|---|---|---|
| restic | CLI backup tool with client‑side encryption, deduplication & fast incremental snapshots | Small binary, no root needed, battle‑tested, easy restores |
| rest‑server | Lightweight HTTP backend that speaks restic’s REST protocol | Lower latency than SFTP/SSH when the repository lives on a remote CIFS/NFS mount (no double encryption, stream‑oriented) |
| Hetzner Storage Box | Cheap off‑site storage available over SMB / Samba, SFTP, WebDAV & more | Flat pricing, runs in the same DC as my (tiny) VPS, unlimited traffic |
| Tailscale | WireGuard‑based overlay network | End‑to‑end encryption, avoids public exposure & lets every client “see” the repo via an internal IP |
| Docker Compose | Deploy rest‑server & its CIFS mount reproducibly | One‑file infrastructure, easy upgrades & rollbacks |
The result is a pull‑based model: every device (Proxmox node, NAS, laptop, …) runs restic and pushes its encrypted chunks to a central rest‑server that lives on my Hetzner VPS. The VPS itself merely forwards bytes to a CIFS share on the Storage Box; it never sees unencrypted data.
2. Provision your Storage Box & sub‑account
- Order a Storage Box (BX line is enough for backups).
- Inside the Hetzner Cloud console create a sub‑account with:
- SMB/CIFS enabled
- Strong, unique password
Note the UNC path; it looks like
//<username>.your-storagebox.de/<username>
Hetzner’s docs confirm the same syntax for CIFS mounting
3. Spin up a tiny VPS & join it to Tailscale
# 1 vCPU, 2 GB RAM is plenty
hcloud server create --type cpx11 --image debian-13 --datacenter fsn1-dc14 \
--name backup-vps
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --accept-routes --hostname restic-hubRecord the Tailscale IPv4 that the node receives (e.g. 100.x.y.z). Only this IP will be exposed from Docker.
4. Deploy rest‑server with Docker Compose
volumes:
data:
driver: local
driver_opts:
type: cifs
device: "//<user>.your-storagebox.de/<user>"
o: "username=<user>,password=<pass>,vers=3.0,uid=1000,gid=1000"
services:
restic-server:
image: restic/rest-server:latest
restart: always
user: "1000:1000"
environment:
- OPTIONS=--append-only --prometheus --log -
ports:
- "<tailscale_ip>:8000:8000"
volumes:
- data:/data
- ./htpasswd:/htpasswd:roWhat’s changed and why:
No more version: "3.9"— Docker Compose V2 no longer requires or recommends theversionkey. Drop it.--append-only— Clients can create new backups but cannot delete or modify existing ones. This protects against ransomware or a compromised client wiping your snapshots. Runforgetandprunefrom the server side only.--prometheus— Exposes metrics at/metricsfor scraping with Prometheus + Grafana. Free observability into repo size, request counts, and error rates.- HTTP basic auth via htpasswd — Instead of
DISABLE_AUTHENTICATION=1, we now mount a.htpasswdfile. Each client gets its own credentials. Generate with:htpasswd -B -c ./htpasswd/credentials <username> - Tailscale IP binding — Still binding to the Tailscale IP instead of
0.0.0.0to prevent accidental public exposure. This is your primary network-level control.
Bring it up:
docker compose up -dVerify the server is running:
curl -u <username>:<password> http://<tailscale_ip>:8000/A 404 page not found response means the server is up but the repository directory is empty—time to initialize it from a client.
5. Install restic on your Proxmox host
apt update && apt install -y restic
# Verify you have 0.18+
restic version
# Store the repo password securely
pwgen -s 32 1 > /etc/restic-key
chmod 600 /etc/restic-keyRestic 0.18 (March 2025) added cold storage support for S3 backends, improved prune performance for repacking small files, and compression for dump output. If your distro ships an older version, grab the latest binary from the GitHub releases.
6. The backup script
Below is the full Bash helper I use. It does three jobs:
- Initializes the repo if it’s the first run.
- Runs
restic backupwith a chosen tag. - Enforces a retention policy (7 daily, 4 weekly, 12 monthly, 2 yearly) via
restic forget --prune.
It also ships Telegram alerts on success/failure so I don’t have to tail logs at 3 am.
#!/usr/bin/env bash
# backup.sh - Backup script using restic
set -euo pipefail
# Repository (adjust to your rest-server)
export RESTIC_REPOSITORY=rest:http://<username>:<password>@<tailscale_ip>:8000
export RESTIC_PASSWORD_FILE=/etc/restic-key
# Retention policy
KEEP_DAILY=7
KEEP_WEEKLY=4
KEEP_MONTHLY=12
KEEP_YEARLY=2
# Telegram notifications
TELEGRAM_CHAT_ID=<telegram_chat_id>
TELEGRAM_BOT_TOKEN=<telegram_bot_token>
telegram_notify() {
local message="$1"
curl -sf -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-d chat_id="${TELEGRAM_CHAT_ID}" \
-d parse_mode="Markdown" \
--data-urlencode text="${message}" >/dev/null 2>&1 || true
}
initialize_repo() {
if restic snapshots >/dev/null 2>&1; then
echo "Repository already initialized."
else
echo "Initializing repository..."
restic init
telegram_notify "\xE2\x84\xB9\xEF\xB8\x8F Initialized restic repo on host: \`$(hostname)\`"
fi
}
backup() {
local source_dir="$1"
local tag="$2"
echo "Backing up ${source_dir} (tag: ${tag})..."
restic backup "${source_dir}" \
--tag "${tag}" \
--exclude-caches \
--verbose
telegram_notify "\xE2\x9C\x85 Backup of \`${source_dir}\` (${tag}) *completed* on \`$(hostname)\`"
}
cleanup() {
echo "Applying retention policy..."
restic forget \
--keep-daily "${KEEP_DAILY}" \
--keep-weekly "${KEEP_WEEKLY}" \
--keep-monthly "${KEEP_MONTHLY}" \
--keep-yearly "${KEEP_YEARLY}" \
--prune
telegram_notify "\xF0\x9F\xA7\xB9 Cleanup completed on \`$(hostname)\`"
}
error_handler() {
telegram_notify "\xE2\x9D\x8C Backup *FAILED* on \`$(hostname)\`: \`${1:-unknown error}\`"
exit 1
}
trap 'error_handler "$BASH_COMMAND"' ERR
# --- Main ---
case "${1:-}" in
backup)
[[ $# -ne 3 ]] && { echo "Usage: $0 backup <dir> <tag>"; exit 1; }
initialize_repo
backup "$2" "$3"
;;
cleanup)
cleanup
;;
*)
echo "Usage: $0 {backup <dir> <tag>|cleanup}"
exit 1
;;
esacChanges from the original script:
- HTTP basic auth credentials are now embedded in the repository URL (
rest:http://user:pass@host:port). --exclude-cachesskips directories containing aCACHEDIR.TAGfile.- The Telegram function has
|| trueto prevent a notification failure from aborting the entire backup. - Replaced
if/elifchain with acasestatement for cleaner argument handling. - The error trap now captures the failing command in the alert message.
7. Scheduling with crontab
Because the script bundles both the backup and retention logic, the crontab ends up extremely simple.
Here’s a practical example (sudo crontab -e on the Proxmox host):
# ┌─ minute (0‑59)
# │ ┌─ hour (0‑23)
# │ │ ┌─ day‑of‑month (1‑31)
# │ │ │ ┌─ month (1‑12)
# │ │ │ │ ┌─ day‑of‑week (0‑7) (0|7 = Sunday)
# │ │ │ │ │
# │ │ │ │ │ command
# │ │ │ │ │
15 3 * * * /usr/local/bin/backup.sh backup /tank/cloud cloud >>/var/log/restic-cloud.log 2>&1
45 3 * * * /usr/local/bin/backup.sh backup /tank/photos photos >>/var/log/restic-photos.log 2>&1
00 4 * * * /usr/local/bin/backup.sh backup /tank/proxmox-backups proxmox >>/var/log/restic-pbs.log 2>&1
# Weekly prune every Sunday at 05:00
00 5 * * 0 /usr/local/bin/backup.sh cleanup >>/var/log/restic-cleanup.log 2>&1
Why cron, not Systemd?
- ✅ Less moving parts — everyone understands crontab.
- ✅ No unit files to manage; one‑liner edits are enough.
- ✅ Logs are redirected to plain files and Telegram, so I still get alerts if something fails.
(If you prefer Systemd, just swap the cron entries for timers — the script itself doesn’t care.)
8. Restoration test (don’t skip this!)
# Verify repository integrity
restic -r rest:http://<user>:<pass>@<tailscale_ip>:8000 \
-p /etc/restic-key \
check
# Restore the latest snapshot to a test directory
restic -r rest:http://<user>:<pass>@<tailscale_ip>:8000 \
-p /etc/restic-key \
restore latest --target /tmp/restore-testRun restic check regularly—it verifies that all data blobs are present and intact. Then do a real restore and verify the output. Boot a VM from the restored vzdump, diff a directory, spot-check a few files. Backups you never test are merely archives.
9. Hardening & extras
- Append-only mode — Already enabled in our Compose file. This is your strongest defense against a compromised client deleting backups. Run
forgetandpruneonly from the server. - HTTP basic auth — One user per client (
htpasswd -B ./htpasswd/credentials <client-name>). Easier to audit and revoke. - Read-only sub-account on the Hetzner Storage Box to prevent deletion at the storage layer too.
- Prometheus + Grafana — Scrape
/metricsfrom rest-server. The official repo includes a ready-made Grafana dashboard. - Rotate the restic password periodically with
restic key passwd. Keep the old key in a secure location until you’ve verified the rotation. - Automatic updates — Use Watchtower or a scheduled
apt upgradeto keep restic and rest-server current. - Firewall — Even with Tailscale, configure
ufwornftableson the VPS to only allow traffic from the Tailscale interface.
10. TL;DR checklist
- 🔐 Generate a strong repository password (
pwgen -s 32). - 🗄️ Order Storage Box → enable SMB → create sub‑account.
- ☁️ Boot a 1 vCPU VPS in the same DC → join Tailscale.
- 🐳 Deploy the
docker‑compose.ymlabove. - 💻 Install restic on every client and drop the Bash script.
- 📅 Schedule with cron (hourly, nightly, weekly).
- 🧪 Restore a random file every month.
- 📈 Add Telegram to get eyes on the process.
Congratulations—you now have an encrypted, versioned, off‑site backup system that costs a few euros a month and scales from single‑board computers to entire datastores.
If you’d prefer a web UI over shell scripts and cron jobs, check out my follow-up post: Zerobyte: A Web UI for restic That Replaces Your Backup Scripts. It uses the same restic engine and can even connect to repositories you’ve already created with this guide.
Happy backing up!