Append-only laptop backups with restic on your NAS

Back up your laptop to a restic rest-server your laptop can write to but never delete from. If ransomware hits the client, your backup history survives.

On this page
  1. Why a plain copy is not enough
  2. Stand up the rest-server on the NAS
  3. Point the laptop at the repository
  4. Automate it with a user timer
  5. Wire a dead-man switch
  6. Prove it actually restores

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.

Append-only: the client adds, the server alone deletesLaptoprestic client + timerencrypts before sendingrest-server on NAS–append-onlyholds ciphertext onlyadd snapshot (allowed)delete / overwrite (rejected)Server-side pruneown credential, own scheduleThe laptop never holds the credential that can delete. Ransomware on it can only add.Retention is a server job, because an append-only client is not allowed to prune its own history

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.

First: make these values your own

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.

1Write the compose file5 min

Pin the image version so an upstream change never surprises you. Publish the internal port 8000 on whatever host port you like.

docker-compose.yml for rest-server

services:
  restic-rest:
    image: restic/rest-server:0.14.0
    restart: always
    environment:
      OPTIONS: "--append-only --private-repos"
    ports:
      - "8500:8000"
    volumes:
      - ./data:/data
2Create the repo user and password5 min

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.

Create the htpasswd entry

htpasswd -B -c ./data/.htpasswd laptop
3Start it and confirm it listens2 min

Bring it up and check the port answers on the NAS.

Launch and probe

docker compose up -d
curl -s -o /dev/null -w '%{http_code}\n' http://10.0.0.50:8500/
Two gotchas the docs never put together

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.

1Create the restic environment file5 min

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.

~/.config/restic/env

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"
2Initialize the repository2 min

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.

Initialize

source ~/.config/restic/env
restic init
3Choose what to skip10 min

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.

~/.config/restic/excludes.txt

**/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.

1Write the backup script10 min

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.

~/.local/bin/laptop-restic-backup.sh

#!/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
2Create the service and timer10 min

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.

~/.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
3Enable it, and let it run while logged out3 min

Enable the timer, then enable lingering so the timer fires even when you are not logged in graphically.

Enable

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.

Restore drill with a checksum compare

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.

The one risk append-only does not cover

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:

Comments

Comments are powered by GitHub Discussions — sign in with a GitHub account to join the conversation.