Backing Up a Hetzner VPS to a Synology NAS with Restic and Tailscale

Hetzner’s built-in backup feature does what it says on the tin, but what it says on the tin isn’t much: full disk image, server must be powered off, maximum of seven backups. That’s fine for a quick pre-maintenance snapshot, but it’s not a backup strategy.

I wanted something running live — no downtime, incremental, encrypted, with a proper retention policy. And I wanted the backups landing on my Synology NAS at home rather than staying on Hetzner’s infrastructure. If something goes badly wrong with my VPS, I’d rather the backups not be sitting next door to the thing that just died.

This is how I set that up, including the bit where I created the SSH authorised keys in completely the wrong place and spent longer than I’d like to admit figuring out why.

The networking problem

My VPS is on the public internet. My NAS is on my home LAN. These two things cannot reach each other directly, and the obvious workaround — port-forwarding SFTP through my home router — would mean exposing my NAS to the internet, which isn’t something I’m willing to do.

The cleaner answer is Tailscale: a zero-config mesh VPN that gives every device you add to it a stable private IP in the 100.x.x.x range. It’s free for personal use, the Synology package is official, and once both devices are connected, they behave as if they’re on the same network regardless of where they physically are. The VPS reaches the NAS over Tailscale; no ports opened on my home firewall.

Why Restic

A few options I considered:

BorgBackup is excellent and pairs well with Borgmatic for YAML-driven automation. If you’re already familiar with it, there’s no strong reason to switch. I chose Restic mainly because it’s available directly from Ubuntu’s package repos, handles SFTP natively, and the command-line interface is clean enough that I can remember it without consulting the docs.

rsync over SSH would work for files but lacks deduplication and encryption. I’d rather not have unencrypted copies of my VPS sitting on a NAS that other household devices can reach.

Hetzner Storage Box would be the path of least resistance if I didn’t already have a NAS. Co-located with the VPS though, which means a Hetzner-wide issue takes out both simultaneously. Not ideal.

Restic won on simplicity, encryption by default, deduplication, and the fact that it stores backups as snapshots I can browse and restore from selectively.

Setting up Tailscale

On the Synology NAS: install the Tailscale package from Package Centre (it’s an official package), sign in.

On the VPS:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

Follow the auth link, signing in with the same Tailscale account. Once both devices appear at https://login.tailscale.com/admin/machines, note your NAS’s Tailscale IP — it appears throughout everything that follows.

Preparing the Synology

In DSM, go to Control Panel → File Services → FTP and enable SFTP on port 22. Then create a dedicated user — I called mine restic-backup — with access scoped to the backup destination folder only. Least privilege, in case anything ever goes sideways.

The part that cost me time: finding where Synology actually puts that user’s home directory. Don’t assume — check:

grep restic-backup /etc/passwd

Mine came back as /var/services/homes/restic-backup, not the /home/restic-backup I’d assumed. Worth knowing before you spend 20 minutes wondering why SSH key auth isn’t working.

Also worth checking: the default shell for DSM-created users is /sbin/nologin, which blocks SSH login even with a valid key. Fix that:

usermod -s /bin/sh restic-backup

SSH key authentication

On the VPS, generate a dedicated key pair for Restic — no passphrase, so it can run unattended:

sudo ssh-keygen -t ed25519 -f /root/.ssh/restic_nas -N ""

On the NAS, set up the .ssh directory in the correct location:

mkdir -p /var/services/homes/restic-backup/.ssh
chmod 700 /var/services/homes/restic-backup/.ssh
touch /var/services/homes/restic-backup/.ssh/authorized_keys
chmod 600 /var/services/homes/restic-backup/.ssh/authorized_keys
chown -R restic-backup:users /var/services/homes/restic-backup/.ssh

Paste the public key from /root/.ssh/restic_nas.pub into authorized_keys, then test from the VPS:

ssh -i /root/.ssh/restic_nas restic-backup@<NAS_TAILSCALE_IP>

If that connects cleanly, you’re ready to proceed.

Installing Restic and initialising the repository

sudo apt update && sudo apt install -y restic

Initialise the repository on the NAS:

restic init \
  -r sftp:restic-backup@<NAS_TAILSCALE_IP>:Backups/ubuntu-8gb-nbg1-1 \
  -o sftp.args="-i /root/.ssh/restic_nas"

Restic will prompt for an encryption password. Store it in a password manager — without it, the backups are inaccessible.

The backup script

sudo nano /usr/local/bin/restic-backup.sh
#!/bin/bash
set -euo pipefail

RESTIC_REPO="sftp:restic-backup@<NAS_TAILSCALE_IP>:Backups/ubuntu-vps"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
LOG_FILE="/var/log/restic-backup.log"

echo "=== Restic backup started: $(date) ===" >> "$LOG_FILE"

# MySQL dump first — captured as part of the filesystem snapshot
DUMP_DIR="/var/backups/mysql"
mkdir -p "$DUMP_DIR"
mysqldump --all-databases --single-transaction \
  > "$DUMP_DIR/all-databases.sql" 2>> "$LOG_FILE"
echo "MySQL dump complete" >> "$LOG_FILE"

restic -r "$RESTIC_REPO" \
  -o sftp.args="-i /root/.ssh/restic_nas" \
  backup / \
  --exclude=/proc \
  --exclude=/sys \
  --exclude=/dev \
  --exclude=/run \
  --exclude=/tmp \
  --exclude=/var/tmp \
  --exclude=/mnt \
  --exclude=/media \
  --exclude=/lost+found \
  --exclude="*.sock" \
  --one-file-system \
  >> "$LOG_FILE" 2>&1

echo "Backup complete" >> "$LOG_FILE"

restic -r "$RESTIC_REPO" \
  -o sftp.args="-i /root/.ssh/restic_nas" \
  forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 3 \
  --prune \
  >> "$LOG_FILE" 2>&1

echo "=== Restic backup finished: $(date) ===" >> "$LOG_FILE"

A couple of things worth calling out: the MySQL dump runs before the filesystem snapshot, so the SQL file is included in the backup in a consistent state. The --one-file-system flag stops Restic from following mount points it shouldn’t. And the retention policy (7 daily, 4 weekly, 3 monthly) gives roughly three months of history without the NAS slowly filling up.

Store the password where the script can read it:

sudo mkdir -p /etc/restic
echo "your-restic-password" | sudo tee /etc/restic/password
sudo chmod 600 /etc/restic/password
sudo chmod 700 /usr/local/bin/restic-backup.sh

Automating with a systemd timer

sudo nano /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic Backup to Synology NAS
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/restic-backup.sh
User=root
sudo nano /etc/systemd/system/restic-backup.timer
[Unit]
Description=Run Restic backup nightly

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

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer

You should see the next scheduled run at 2:00am.

Running the first backup

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

The first run on my VPS backed up 5.1 GiB of files and stored them as 1.955 GiB — Restic’s deduplication already working even on an initial snapshot. Subsequent runs will only transfer what’s changed.

Verify the snapshot was recorded:

restic -r sftp:restic-backup@<NAS_TAILSCALE_IP>:Backups/ubuntu-vps \
  -o sftp.args="-i /root/.ssh/restic_nas" \
  --password-file /etc/restic/password \
  snapshots

Restoring

To restore a specific file:

restic -r sftp:restic-backup@<NAS_TAILSCALE_IP>:Backups/ubuntu-vps \
  -o sftp.args="-i /root/.ssh/restic_nas" \
  --password-file /etc/restic/password \
  restore latest --target /tmp/restore --include /etc/nginx/nginx.conf

To restore everything:

restic -r sftp:restic-backup@<NAS_TAILSCALE_IP>:Backups/ubuntu-vps \
  -o sftp.args="-i /root/.ssh/restic_nas" \
  --password-file /etc/restic/password \
  restore latest --target /

What I’d do differently

Check the Synology user’s actual home directory before touching anything. It’s in /etc/passwd, takes five seconds to look up, and would have saved the better part of an hour. Don’t assume it matches what the DSM UI implies.

Also check the shell. The nologin shell issue was entirely predictable with hindsight. New DSM users default to it. It’s even in the passwd output if you look. I just didn’t look.

Stack at a glance

  • VPS: Hetzner, Ubuntu
  • Backup tool: Restic (from Ubuntu package repos)
  • Transport: SFTP over Tailscale
  • Destination: Synology NAS (/volume1/Backups/ubuntu-vps)
  • Scheduling: systemd timer, nightly at 2am
  • Retention: 7 daily, 4 weekly, 3 monthly snapshots

Closing thoughts

The end result is what I wanted: nightly encrypted backups of the full VPS — including a consistent MySQL dump — landing on local hardware I control, over a private network with no ports opened on my home firewall. Restic handles the deduplication and retention, the systemd timer handles the scheduling, and there’s nothing left to remember.

The debugging detours were self-inflicted and entirely avoidable. But that seems to be the reliable way to actually learn how these things fit together.