On this page
Back up your laptop to a server it can write to but never delete from. That one property, called append-only, is the difference between a backup and a backup that survives the thing you are backing up getting hacked. This guide stands up a restic rest-server on a NAS in append-only mode, points a laptop at it, automates the whole thing with a dead-man switch, and proves it restores. No root on the laptop, client-side encryption, and history a compromised machine cannot erase.
I built this because of an embarrassing gap. My lab had three backup tiers protecting the servers, the containers, and the photos, and then one day I realized the machine I actually work on every day, the laptop with my half-finished projects and my configs and my scripts, was backed up by nothing. It was the last unprotected machine in the house, and it was the one most likely to catch something nasty from the open internet.
Why a plain copy is not enough
The obvious fix is to rsync the laptop to a share on the NAS. The problem is that a copy the laptop can write is a copy the laptop can also destroy. Ransomware does exactly that: it encrypts your files and then hunts down and encrypts or deletes any backups it can reach, including network shares. A synced copy on the NAS is right in its blast radius.
Append-only flips the trust. The client is allowed to push new data and nothing else. The server refuses every delete and overwrite, so the worst a compromised laptop can do is add more snapshots. Your existing history stays intact. That is the whole idea, and restic plus rest-server give it to you with deduplication and client-side encryption on top, so the NAS only ever holds encrypted blobs it cannot read.
The commands below use placeholders. Replace 10.0.0.50 with your NAS’s IP, 8500 with the port you publish, the repo username laptop with a name for this machine, and every CHANGE_ME with a real secret kept in your password manager, never pasted into a note. If a value looks specific to one machine, it’s a placeholder to change, not a literal to copy.
Stand up the rest-server on the NAS
rest-server ships as a container. The one non-obvious thing is that its Docker image always sets --path /data and --htpasswd-file /data/.htpasswd for you through its entrypoint, so the OPTIONS variable is only for extra flags on top of those. You add the two that matter here: --append-only and --private-repos.
Pin the image version so an upstream change never surprises you. Publish the internal port 8000 on whatever host port you like.
services:
restic-rest:
image: restic/rest-server:0.14.0
restart: always
environment:
OPTIONS: "--append-only --private-repos"
ports:
- "8500:8000"
volumes:
- ./data:/data
With --private-repos, a user can only reach the repository whose path matches their username. So a user named laptop uses the repo at /laptop and is denied everything else. Create a bcrypt htpasswd entry for that user.
htpasswd -B -c ./data/.htpasswd laptop
Bring it up and check the port answers on the NAS.
docker compose up -d
curl -s -o /dev/null -w '%{http_code}\n' http://10.0.0.50:8500/
Because the entrypoint already supplies --path and --htpasswd-file, putting them in OPTIONS again just double-sets them; leave them out. And --private-repos means the repository path is the username, so there is no separate repo name to invent. The URL you give restic ends in /laptop, and that has to match the htpasswd user exactly.
Point the laptop at the repository
On the laptop, restic runs as your normal user from a user-space binary. Keep the credentials out of the repository URL by putting the rest-server username and password in environment variables, which restic reads natively: RESTIC_REST_USERNAME and RESTIC_REST_PASSWORD. That also keeps them out of any error message that echoes the URL.
Store the repository address and both credential pairs in one file with tight permissions. The repository password encrypts your data; the rest-server password authenticates to the server.
export RESTIC_REPOSITORY="rest:http://10.0.0.50:8500/laptop"
export RESTIC_PASSWORD_FILE="$HOME/.config/restic/repo-pass"
export RESTIC_REST_USERNAME="laptop"
export RESTIC_REST_PASSWORD="CHANGE_ME"
Source the env file and initialize once. The repository password you set here is the one you must be able to recover later, so save it somewhere off the laptop.
source ~/.config/restic/env
restic init
Back up your home directory and exclude the noise: caches, downloads, build artifacts, and anything already protected elsewhere. --one-file-system stops restic from wandering into mounted filesystems.
**/node_modules
**/.venv
**/target
**/.terraform
$HOME/.cache
$HOME/Downloads
$HOME/.local/share/Trash
Automate it with a user timer
A backup you have to remember to run is a backup you will forget to run. A systemd user timer schedules it inside your account with no root involved. Two details make it dependable: Persistent=true catches up after the laptop has been closed for days, and an ExecCondition that probes the server first means a day spent off the network skips cleanly instead of logging a failure.
One script does the whole cycle: back up, verify the repository with restic check, then signal success. Only signal after both the backup and the check pass, so a corrupt repository still trips the alarm.
#!/usr/bin/env bash
set -euo pipefail
source "$HOME/.config/restic/env"
restic backup "$HOME" \
--exclude-file "$HOME/.config/restic/excludes.txt" \
--one-file-system
restic check
curl -fsS "$KUMA_PUSH_URL" >/dev/null
The service probes the server before running. The timer fires daily with a little jitter so every machine does not hit the NAS at the same second.
[Service]
Type=oneshot
ExecCondition=curl -fsS -o /dev/null http://10.0.0.50:8500/
ExecStart=%h/.local/bin/laptop-restic-backup.sh
[Timer]
OnCalendar=*-*-* 12:00:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
Enable the timer, then enable lingering so the timer fires even when you are not logged in graphically.
systemctl --user enable --now laptop-restic.timer
loginctl enable-linger $USER
Wire a dead-man switch
A silent backup is the dangerous kind: it stops working and you find out months later when you need it. Invert the alarm. The script pings a push monitor only on success, and the monitor shouts if it does not hear from it. Uptime Kuma does this with a push monitor: set the heartbeat window a little longer than a day, point the KUMA_PUSH_URL in your env file at it, and route its alert to ntfy or your channel of choice. No heartbeat in 25 hours means no good backup happened, and you hear about it the same day.
Prove it actually restores
The most common backup mistake is never testing the restore. A repository that accepts backups can still fail to give them back. Do a real drill: restore one file to a scratch directory and compare it byte for byte against the original.
source ~/.config/restic/env
restic restore latest --target /tmp/restore-test --include "$HOME/.config/restic/env"
sha256sum "$HOME/.config/restic/env" /tmp/restore-test/$HOME/.config/restic/env
If the two hashes match, you have a real backup. One more thing to internalize: “it boots” is not a restore test, and neither is “the job says success.” Only a checksum compare of restored data proves the bytes came back, as long as both hashes are really there: I once had a hash check pass on two empty strings. And because restic encrypts client-side, the repository password is the whole ballgame; keep it somewhere you can reach even if this laptop is a brick, or the ciphertext on the NAS is unrecoverable. Once this works, the same append-only server can protect the NAS’s own app data, which adds one wrinkle a laptop never has: a live database.
Append-only protects you from a compromised client. It does nothing about someone with access to the server. The NAS now holds the only deletable copy of your history, so the server’s own security, and ideally its own off-box backup, is what protects the protector.
Related posts:
- Measure how fast your backups actually restore — the restore drill discipline, applied to the whole cluster.
- What backs up your NAS apps? Append-only restic — the same append-only pattern, pointed at the NAS’s own app data.
- Stop Proxmox Backup Server flooding your inbox — quieting backup noise without losing the failure alerts that matter.
- Self-healing arr stack — the same dead-man-switch pattern on a different service.
- What is Prometheus? — the monitoring that catches a silently-dead backup job.
- Tailscale subnet router for your homelab — how the laptop reaches the NAS securely from anywhere.
- Create your first Proxmox LXC container — where a rest-server lives if you run it on the cluster instead of a NAS.
Comments
Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.