Append-only restic rest-server: NAS target + laptop client
Deploy an append-only restic rest-server on a NAS and back a laptop up to it with a systemd user timer, a dead-man switch, and a restore drill.
bash#!/usr/bin/env bash
# Laptop -> append-only restic rest-server on the NAS. Runs daily via a systemd user timer.
set -euo pipefail
source "$HOME/.config/restic/env" # RESTIC_REPOSITORY, RESTIC_PASSWORD_FILE,
# RESTIC_REST_USERNAME/PASSWORD, KUMA_PUSH_URL
restic backup "$HOME" \
--exclude-file "$HOME/.config/restic/excludes.txt" \
--one-file-system
restic check # verify repo integrity before signalling success
curl -fsS "$KUMA_PUSH_URL" >/dev/null # dead-man heartbeat: pinged only after backup AND check pass
What this does
Stands up an append-only restic rest-server on a NAS and backs a laptop’s home directory up to it on a daily systemd user timer. The client can add snapshots but never delete or overwrite them, so a compromised laptop cannot destroy its own backup history. restic encrypts client-side, so the NAS holds ciphertext only, and retention runs server-side because an append-only client is not allowed to prune. The script above is the daily backup job; the server and client setup are below.
Prerequisites
- A NAS or host that runs Docker, reachable from the laptop (directly or over a VPN like Tailscale).
- restic as a user-space binary on the laptop (no root needed).
- An Uptime Kuma push monitor for the dead-man switch (optional but recommended).
1. Deploy the rest-server on the NAS
The Docker image’s entrypoint always supplies --path /data and --htpasswd-file /data/.htpasswd, so OPTIONS only carries the extra flags.
# docker-compose.yml
services:
restic-rest:
image: restic/rest-server:0.14.0
restart: always
environment:
OPTIONS: "--append-only --private-repos"
ports:
- "8500:8000"
volumes:
- ./data:/data
Create the repo user. With –private-repos the repository path equals the username, so user laptop uses the repo /laptop.
htpasswd -B -c ./data/.htpasswd laptop
docker compose up -d
2. Configure the laptop client
Keep credentials out of the URL with restic’s REST environment variables.
# ~/.config/restic/env (chmod 600)
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"
export KUMA_PUSH_URL="https://kuma.homelab.lan/api/push/CHANGE_ME"
# ~/.config/restic/excludes.txt
**/node_modules
**/.venv
**/target
**/.terraform
$HOME/.cache
$HOME/Downloads
$HOME/.local/share/Trash
Initialize the repository once (save the repo password somewhere off the laptop).
source ~/.config/restic/env
restic init
3. Schedule it with a systemd user timer
The service probes the server first, so off-network days skip cleanly instead of failing.
# ~/.config/systemd/user/laptop-restic.service
[Service]
Type=oneshot
ExecCondition=curl -fsS -o /dev/null http://10.0.0.50:8500/
ExecStart=%h/.local/bin/laptop-restic-backup.sh
# ~/.config/systemd/user/laptop-restic.timer
[Timer]
OnCalendar=*-*-* 12:00:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
systemctl --user enable --now laptop-restic.timer
loginctl enable-linger $USER
4. Prove it restores
A job that says “success” is not a restore test. Restore one file and compare hashes.
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
Notes
- Make these values your own. Replace
10.0.0.50with your NAS IP,8500with your published port, the repo usernamelaptopwith a name for this machine,kuma.homelab.lanwith your Kuma host, and everyCHANGE_MEwith a real secret kept in your password manager. If a value looks specific to one machine, it’s a placeholder to change, not a literal to copy. - Two rest-server gotchas nobody documents together: the Docker entrypoint already injects
--pathand--htpasswd-file, so leave them out ofOPTIONS; and--private-reposmakes the repo path equal the htpasswd username, so the URL ends in/laptopand there is no separate repo name. - Retention is server-side. Append-only clients cannot prune, so run
restic forget --pruneon the server (or a host with a separate credential) on a schedule, never from the laptop. - The repository password is the whole ballgame. restic encrypts client-side; without that password the ciphertext on the NAS is unrecoverable, so escrow it off the laptop.
- Append-only protects against a compromised client, not a compromised server. Protect the NAS itself, and ideally back the repository up off-box too.
- Verified on the real setup 2026-08-15: repo init, full home-directory seed, and a restore drill with a sha256 match.