> ## Content Index
> Fetch the complete content index at: https://fathi.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# Building a Fast, Encrypted & Private Backup Pipeline with restic, rest‑server, Hetzner Storage Box & Tailscale VPN
- URL: https://fathi.me/building-a-fast-encrypted-private-backup-pipeline-with-restic-rest-server-hetzner-storage-box-tailscale-vpn/
- Published: 2025-04-17T20:00:15.000Z
- Updated: 2026-04-06T10:42:52.000Z
- Description: 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.
- Author: Yassine Fathi
- Tags: Homelab, Posts

**Updated April 2026** — Refreshed for restic 0.18, improved Docker Compose (no deprecated `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](https://fathi.me/zerobyte-restic-web-ui-backup-automation/).

> 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](https://l.fathi.me/hetzner?ref=fathi.me). 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

1. Order a Storage Box (BX line is enough for backups).
2. 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

```bash
# 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-hub
```

Record 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

```yaml
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:ro
```

**What’s changed and why:**

- `No more version: "3.9"` — Docker Compose V2 no longer requires or recommends the `version` key. 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. Run `forget` and `prune` from the server side only.
- `--prometheus` — Exposes metrics at `/metrics` for 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 `.htpasswd` file. 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.0` to prevent accidental public exposure. This is your primary network-level control.

Bring it up:

```bash
docker compose up -d
```

Verify the server is running:

```bash
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

```bash
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-key
```

Restic 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](https://github.com/restic/restic/releases?ref=fathi.me).

---

### 6\. The backup script

Below is the full Bash helper I use. It does three jobs:

1. **Initializes** the repo if it’s the first run.
2. Runs `restic backup` with a chosen tag.
3. 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.

```bash
#!/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
    ;;
esac
```

**Changes from the original script:**

- HTTP basic auth credentials are now embedded in the repository URL (`rest:http://user:pass@host:port`).
- `--exclude-caches` skips directories containing a `CACHEDIR.TAG` file.
- The Telegram function has `|| true` to prevent a notification failure from aborting the entire backup.
- Replaced `if/elif` chain with a `case` statement 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):

```bash
# ┌─ 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!)

```bash
# 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-test
```

Run `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 `forget` and `prune` only 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 `/metrics` from rest-server. The official repo includes a [ready-made Grafana dashboard](https://github.com/restic/rest-server/tree/master/examples/compose-with-grafana?ref=fathi.me).
- **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 upgrade` to keep restic and rest-server current.
- **Firewall** — Even with Tailscale, configure `ufw` or `nftables` on the VPS to only allow traffic from the Tailscale interface.

---

### 10\. TL;DR checklist

1. 🔐 Generate a strong repository password (`pwgen -s 32`).
2. 🗄️ [Order Storage Box](https://l.fathi.me/hetzner?ref=fathi.me) → enable SMB → create sub‑account.
3. ☁️ Boot a 1 vCPU VPS in the same DC → join Tailscale.
4. 🐳 Deploy the `docker‑compose.yml` above.
5. 💻 Install restic on every client and drop the Bash script.
6. 📅 Schedule with cron (hourly, nightly, weekly).
7. 🧪 Restore a random file every m*onth.*
8. *📈 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](https://fathi.me/zerobyte-restic-web-ui-backup-automation/). It uses the same restic engine and can even connect to repositories you’ve already created with this guide.

Happy backing up!