LinuxstorageTested on real hardware

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.

DistrosopenSUSE Tumbleweed, Ubuntu 24.04
Shellbash
Updated
Script
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.

yaml

# 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.

bash

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.

bash

# ~/.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"
text

# ~/.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).

bash

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.

ini

# ~/.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
ini

# ~/.config/systemd/user/laptop-restic.timer
[Timer]
OnCalendar=*-*-* 12:00:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
bash

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.

bash

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.50 with your NAS IP, 8500 with your published port, the repo username laptop with a name for this machine, kuma.homelab.lan with your Kuma host, and every CHANGE_ME with 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 --path and --htpasswd-file, so leave them out of OPTIONS; and --private-repos makes the repo path equal the htpasswd username, so the URL ends in /laptop and there is no separate repo name.
  • Retention is server-side. Append-only clients cannot prune, so run restic forget --prune on 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.