π

Framework-Notebook mit restic aufs Ugreen-NAS sichern

Show Sidebar

Mein Framework 13 läuft seit einiger Zeit mit Kubuntu, aber zu meiner Schande hatte ich noch keine richtige Backup-Strategie eingerichtet. Seit dem Umzug auf das neue Ugreen-NAS ist dort genug Platz, also war es Zeit, das nachzuholen.

Meine Wünsche waren:

In diesem Artikel zeige ich, wie ich das mit restic, einem rest-server im Docker auf dem NAS und zwei systemd-Timern umgesetzt habe. Am Ende steht ein längerer Abschnitt zur Fehlersuche, denn der erste Lauf war zunächst quälend langsam.

Die Idee

Ablaufdiagramm vom Notebook über LAN oder Tailscale zum rest-server auf dem NAS und weiter zur USB-Platte
Der Weg der Daten: zuhause direkt übers LAN, unterwegs über Tailscale (klicken für größere Version)

Am Laptop läuft restic als root, gestartet von systemd-Timern. Es schickt die verschlüsselten Daten an einen rest-server, der als Container auf dem NAS läuft und alles in einer eigenen Freigabe ablegt. Zuhause geht das direkt über das Heimnetz, unterwegs über Tailscale. Nachts spiegelt das NAS die Freigabe zusätzlich auf eine USB-Platte.

Warum gerade so:

Es gibt zwei Sicherungen, unterschieden über restic-Tags:

Tag Was Wann
home /home/benutzer, /etc, /root, /usr/local, Paketlisten einmal pro Tag
system das ganze / inklusive /boot und /boot/efi, ohne /home einmal pro Woche

Die täglichen Konfigurationen in /etc sind damit doppelt drin, aber das kostet dank Deduplizierung nichts.

Voraussetzungen

Teil 1: Das NAS

Freigabe anlegen

In UGOS unter „Systemsteuerung“ → „Freigegebener Ordner“ lege ich eine neue Freigabe fw13-backup an. Wichtig sind dabei zwei Einstellungen:

Bei der Gelegenheit ist mir aufgefallen, dass mein normaler Benutzer auch auf die Freigabe docker Lesen und Schreiben hatte. Dort liegen Zugangsdaten und Konfigurationen meiner Container. Das habe ich gleich mit abgestellt.

rest-server im Docker

Die Compose-Datei liegt bei mir unter /volume1/docker/rest-server/docker-compose.yaml:

services:
  rest-server:
    image: restic/rest-server:0.14.0
    container_name: rest-server
    restart: unless-stopped
    ports:
      - "8000:8000"   # LAN direkt (zuhause schnell), unterwegs über Tailscale
    volumes:
      - /volume1/fw13-backup:/data
    environment:
      OPTIONS: "--append-only --private-repos"	  

--private-repos sorgt dafür, dass jeder Benutzer nur in sein eigenes Unterverzeichnis schreiben darf. Der Benutzer fw13 kommt also nur an /fw13/ heran. Kommen später weitere Notebooks dazu, bekommt jedes einen eigenen Benutzer.

Ursprünglich hatte ich den Port auf 127.0.0.1:8000 beschränkt, damit der rest-server nur über Tailscale erreichbar ist. Warum er jetzt doch im LAN offen ist, steht weiter unten bei der Fehlersuche.

Starten und einen Benutzer anlegen:

  cd /volume1/docker/rest-server
  docker compose up -d
  docker exec -it rest-server create_user fw13	  

Das Passwort für den rest-server ist nicht das restic-Passwort. Du kannst es mit openssl rand -hex 16 oder einem anderen Zufallsgenerator erzeugen und speichere es in einen Passwortmanager. Am besten eines ohne Sonderzeichen, dann gibt es später keinen Ärger mit Anführungszeichen.

Die Warnung „No user exists“ im Log beim ersten Start ist normal, sie stammt aus der Zeit vor create_user.

Für Tailscale freigeben

Für unterwegs leite ich einen eigenen HTTPS-Port im Tailnet auf den rest-server weiter. Bei mir ist Port 8443 schon für etwas anderes belegt, deshalb 8444:

  docker exec tailscale tailscale serve --bg --https=8444 http://127.0.0.1:8000
  docker exec tailscale tailscale serve status	  

Das ist nur im Tailnet sichtbar, nicht im Internet (dafür wäre tailscale funnel zuständig). Die Einstellung bleibt auch nach einem Neustart erhalten.

Ein schneller Test vom Laptop aus. Hier und im Folgenden steht nas-name.<tailnet>.ts.net für die Tailscale-Adresse des NAS. Die Heimnetz-Adresse 192.168.1.10, der Benutzer benutzer und die Ablageorte der Passwort- und Zugangsdateien (repo-kennwort und zugang.env am Laptop, kennwort-fw13 am NAS) sind Beispiele, die du an dein System anpassen musst:

  curl -u fw13 --max-time 15 https://nas-name.<tailnet>.ts.net:8444/fw13/config	  

Die Antwort Not Found ist hier richtig, denn das Repository gibt es noch nicht. 401 heißt falsches Passwort, und „Connection refused“ heißt, dass die Weiterleitung fehlt.

Teil 2: Der Laptop

Alles läuft als root, damit restic auch /etc und die Systemdateien lesen kann.

restic und Passwörter

  sudo apt install restic curl
  restic version	  

Dann das Passwort für das Repository. Ohne dieses Passwort ist das Backup wertlos, es gehört also sofort in den Passwortmanager:

  sudo install -d -m 700 /etc/restic
  openssl rand -base64 32 | sudo tee /etc/restic/repo-kennwort
  sudo chmod 600 /etc/restic/repo-kennwort	  

Die Zugangsdatei

Alle Einstellungen stehen in /etc/restic/zugang.env (danach sudo chmod 600 /etc/restic/zugang.env):

  RESTIC_REPOSITORY=rest:https://nas-name.<tailnet>.ts.net:8444/fw13/
  RESTIC_REST_USERNAME=fw13
  RESTIC_REST_PASSWORD=<Passwort für den rest-server>
  RESTIC_PASSWORD_FILE=/etc/restic/repo-kennwort
  RESTIC_CACHE_DIR=/var/cache/restic
  RESTIC_PROGRESS_FPS=0.0166   # Fortschritt etwa 1x pro Minute im Journal
  # zuhause direkt per LAN
  RESTIC_REPOSITORY_LAN=rest:http://192.168.1.10:8000/fw13/
  NAS_MAC=xx:xx:xx:xx:xx:xx	  

Zu den letzten Zeilen:

Damit ich nicht jedes Mal alle Variablen setzen muss, gibt es einen kleinen Kurzbefehl /usr/local/sbin/restic-fw13:

  #!/bin/sh
  set -a; . /etc/restic/zugang.env; set +a
  exec restic "$@"	  
  sudo chmod 755 /usr/local/sbin/restic-fw13
  sudo restic-fw13 init	  

Ab jetzt gehen sudo restic-fw13 snapshots, restore und so weiter ohne Tipparbeit.

Was nicht ins Backup soll

/etc/restic/excludes-home.txt:

/home/benutzer/.cache
/home/benutzer/.local/share/Trash
/home/benutzer/.local/share/baloo
/home/benutzer/.thumbnails
/home/benutzer/.var/app/*/cache
/home/benutzer/snap/*/*/.cache
/home/benutzer/.local/share/Steam
/home/benutzer/KI/model
/root/.cache	  

Steam-Spiele und KI-Modelle kann man jederzeit neu herunterladen, die müssen nicht ins Backup. Meine Fotos und Videos bleiben drin, die liegen nur auf dem Laptop.

/etc/restic/excludes-system.txt:

/home
/tmp
/var/tmp
/var/cache
/var/lib/snapd/cache
/var/log/journal
/swap.img
/swapfile	  

/proc, /sys, /dev und /run fallen durch --one-file-system automatisch weg. Weil /boot und /boot/efi bei mir eigene Partitionen sind (das legt der Kubuntu-Installer bei verschlüsselter Installation so an), stehen sie im Skript ausdrücklich als Quellen. Ob es sie gibt, zeigt findmnt -M /boot bzw. findmnt -M /boot/efi.

Das Backup-Skript

Das Herzstück ist /usr/local/sbin/restic-backup.sh (danach sudo chmod 755). Es bekommt als Parameter home oder system:

  #!/bin/bash
  # Aufruf: restic-backup.sh home|system
  set -uo pipefail
  MODE="${1:-}"
  set -a; . /etc/restic/zugang.env; set +a
  STATE=/var/lib/restic-backup
  mkdir -p "$STATE"

  case "$MODE" in
      home)   MAXAGE=$((20*3600)) ;;     # höchstens 1x pro Tag
      system) MAXAGE=$((6*24*3600)) ;;   # höchstens 1x pro Woche
      ,*) echo "Aufruf: $0 home|system"; exit 2 ;;
  esac

  # Letzte erfolgreiche Sicherung noch frisch? Dann nichts tun.
  if [ -f "$STATE/$MODE.last" ] && \
         [ $(( $(date +%s) - $(stat -c %Y "$STATE/$MODE.last") )) -lt $MAXAGE ]; then
      echo "$MODE: letzte Sicherung ist noch aktuell"; exit 0
  fi

  # Nur am Netzteil
  if ! systemd-ac-power; then echo "Akkubetrieb, übersprungen"; exit 0; fi

  # Zuhause? Dann direkt übers LAN, der Tailscale-Proxy ist beim Hochladen langsam
  if [ -n "${RESTIC_REPOSITORY_LAN:-}" ] && ping -c1 -W1 192.168.1.10 >/dev/null 2>&1 \
         && ip neigh show 192.168.1.10 | grep -qi "${NAS_MAC:-none}"; then
      export RESTIC_REPOSITORY="$RESTIC_REPOSITORY_LAN"; echo "Weg: LAN"
  else
      echo "Weg: Tailscale"
  fi

  # NAS erreichbar?
  if ! restic cat config >/dev/null; then
      echo "NAS nicht erreichbar, nächster Versuch beim nächsten Timer"; exit 0
  fi

  if [ "$MODE" = home ]; then
      # Paketlisten für die Neuinstallation
      P=/var/backups/pakete
      mkdir -p "$P"
      dpkg --get-selections > "$P/dpkg-selections.txt"
      apt-mark showmanual   > "$P/apt-manuell.txt"
      command -v snap    >/dev/null && snap list > "$P/snap.txt"
      command -v flatpak >/dev/null && flatpak list --app --columns=application > "$P/flatpak.txt"
      lsblk -f > "$P/lsblk.txt"; cp /etc/fstab "$P/fstab"

      restic backup -o rest.connections=20 --tag home --exclude-caches \
  	   --exclude-file=/etc/restic/excludes-home.txt \
  	   /home/benutzer /etc /root /usr/local /var/backups/pakete
      RC=$?
  else
      restic backup -o rest.connections=20 --tag system --one-file-system \
  	   --exclude-file=/etc/restic/excludes-system.txt \
  	   / /boot /boot/efi
      RC=$?
  fi

  # 0 = ok, 3 = ok, aber einzelne Dateien nicht lesbar
  if [ $RC -eq 0 ] || [ $RC -eq 3 ]; then
      touch "$STATE/$MODE.last"
      echo "$MODE: fertig (Code $RC)"
      exit 0
  fi
  echo "$MODE: FEHLER (Code $RC)"; exit $RC	  

Ein paar Gedanken dazu:

Teil 3: Zeitplan mit systemd

Ein Dienst für beide Sicherungen, /etc/systemd/system/restic-backup@.service:

[Unit]
Description=restic-Sicherung (%i) aufs NAS
Wants=network-online.target
After=network-online.target tailscaled.service
ConditionACPower=true

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-backup.sh %i
Nice=10
IOSchedulingClass=idle	  

Dazu zwei Timer. /etc/systemd/system/restic-backup-home.timer:

[Unit]
Description=Home-Sicherung, Versuch alle 3 Stunden

[Timer]
OnCalendar=*-*-* 09,12,15,18,21:00
Persistent=true
RandomizedDelaySec=10min
Unit=restic-backup@home.service

[Install]
WantedBy=timers.target	  

Und /etc/systemd/system/restic-backup-system.timer mit denselben Zeiten, eine halbe Stunde versetzt. Dass das System nur einmal pro Woche gesichert wird, regelt das Skript:

[Unit]
Description=System-Sicherung, wöchentlich

[Timer]
OnCalendar=*-*-* 09,12,15,18,21:30
Persistent=true
RandomizedDelaySec=10min
Unit=restic-backup@system.service

[Install]
WantedBy=timers.target	  

Persistent=true holt Termine nach, die der Laptop im Ruhezustand verpasst hat. Der erste Lauf von Hand, und danach die Timer einschalten:

  sudo systemctl daemon-reload
  sudo systemctl start --no-block restic-backup@home.service
  journalctl -fu restic-backup@home.service

  sudo systemctl enable --now restic-backup-home.timer restic-backup-system.timer
  systemctl list-timers 'restic*'	  

Ohne --no-block wartet systemctl start bis zum Ende der Sicherung, und das kann beim ersten Mal Stunden dauern.

Terminal mit journalctl-Ausgabe einer restic-Sicherung
Der erste System-Lauf im Journal (gekürzt) (klicken für größere Version)

Läuft gerade eine Sicherung, schreibt restic auf Zuruf sofort eine Fortschrittszeile ins Journal:

  sudo pkill -USR1 -x restic
  journalctl -u restic-backup@home.service -n 3 -o cat	  

Im Terminal selbst erscheint dabei nichts, die Zeile steht im Journal.

Der Bildschirm darf sich dabei übrigens sperren und der Laptop zugeklappt sein. Die Sicherung läuft als Systemdienst unabhängig von der Sitzung weiter, solange der Laptop nicht in den Ruhezustand geht.

Teil 4: Aufräumen und zweite Kopie auf dem NAS

Wegen append-only kann der Laptop keine alten Sicherungen löschen. Das übernimmt das NAS mit einem eigenen restic. Dafür liegt das Repository-Passwort des Laptops auf dem NAS in /volume1/docker/restic/kennwort-fw13 (chmod 600).

Das Skript /volume1/docker/restic/prune-notebooks.sh:

  #!/bin/sh
  R=/volume1/docker/restic/bin/restic
  LOG=/volume1/docker/restic/prune-notebooks.log
  for nb in fw13; do
      echo "== $(date '+%F %T') $nb" >> "$LOG"
      $R -r /volume1/fw13-backup/$nb \
         --password-file /volume1/docker/restic/kennwort-$nb --retry-lock 30m \
         forget --group-by host,tags \
         --keep-daily 4 --keep-weekly 2 --keep-monthly 4 --prune >> "$LOG" 2>&1
      # erste Woche im Monat: 5 % der Daten prüfen
      if [ "$(date +%d)" -le 07 ]; then
  	$R -r /volume1/fw13-backup/$nb \
  	   --password-file /volume1/docker/restic/kennwort-$nb \
  	   check --read-data-subset=5% >> "$LOG" 2>&1
      fi
  done	  

--group-by host,tags ist wichtig: Damit gilt die Regel 4/2/4 getrennt für home und system. Bei system gibt es nur eine Sicherung pro Woche, dort bleiben also die letzten vier Wochen und vier Monatsstände.

Per Cron als root läuft es jeden Sonntag um 14:00, weit weg vom nächtlichen NAS-Backup:

0 14 * * 0 /volume1/docker/restic/prune-notebooks.sh	  

Läuft gerade eine Sicherung vom Laptop, wartet das Aufräumen dank --retry-lock bis zu 30 Minuten.

Mein NAS sichert sich nachts selbst mit restic auf eine USB-Platte. Das Notebook-Repository habe ich dort aber bewusst nicht als weitere Quelle eingetragen. Eine geänderte Quellenliste hätte ein Neueinlesen aller 3 TB ausgelöst, und das Repository ist ohnehin schon verschlüsselt. Stattdessen spiegelt eine zusätzliche Zeile im nächtlichen Skript die Freigabe 1:1 auf die Platte:

  rsync -a --delete /volume1/fw13-backup/ "$MNT/fw13-backup/" && echo "fw13-backup gespiegelt"	  

$MNT ist dabei der Einhängepunkt der USB-Platte, den das Skript vorher ermittelt.

Teil 5: Prüfen und Test-Wiederherstellung

Ein Backup zählt erst, wenn eine Wiederherstellung einmal geklappt hat. Nach dem ersten Lauf und danach etwa alle drei Monate:

Was Befehl am Laptop
Sicherungen anzeigen sudo restic-fw13 snapshots
Letzte Läufe und Fehler journalctl -u 'restic-backup@*' --since -3d
Nächste Termine systemctl list-timers 'restic*'
Einzelne Datei zurückholen sudo restic-fw13 restore latest --tag home --include /home/benutzer/.zshrc --target /tmp/rtest
Durchsuchen wie ein Laufwerk sudo restic-fw13 mount /mnt/restic (Ende mit Strg+C)
Platzverbrauch sudo restic-fw13 stats --mode raw-data

Meine Test-Datei war die .zshrc. Ein diff mit dem Original war leer, also alles gut.

Auch append-only sollte man einmal testen: sudo restic-fw13 forget <ID> mit einer ID aus snapshots muss am Laptop mit einem Fehler abbrechen.

Im Notfall

Dann brauche ich nur zwei Einträge aus dem Passwortmanager: das restic-Passwort und das Passwort für den rest-server. Auf dem neuen System oder einem Live-USB installiere ich restic (und für unterwegs Tailscale) und lege /etc/restic/zugang.env, /etc/restic/repo-kennwort und restic-fw13 wie oben an.

Der einfachste Weg ist eine frische Installation plus Daten:

  sudo restic-fw13 restore latest --tag home --target /var/tmp/r
  xargs -a /var/tmp/r/var/backups/pakete/apt-manuell.txt sudo apt install -y
  xargs -a /var/tmp/r/var/backups/pakete/flatpak.txt flatpak install -y
  sudo rsync -aH /var/tmp/r/home/benutzer/ /home/benutzer/	  

Aus /var/tmp/r/etc übernehme ich dann gezielt, was ich selbst geändert hatte. fstab, crypttab und /etc/default/grub kopiere ich nicht blind, die gehören zur neuen Installation.

Alternativ geht auch das komplette System 1:1 aus der wöchentlichen Sicherung: vom Live-USB Partitionen anlegen (Vorlage ist die gesicherte lsblk.txt), erst system und dann home nach /mnt wiederherstellen, die UUIDs in fstab und crypttab anpassen und im chroot Initramfs und GRUB neu bauen. Das lohnt sich aber nur, wenn viel am System selbst eingerichtet war.

Fehlersuche: Warum war der erste Lauf so langsam?

Eingerichtet war alles in einer guten Stunde. Dann kam der erste Home-Lauf mit knapp 400 GiB, und restic meldete:

[41:01] 1.37%  32650 files 5.680 GiB, total 276787 files 414.167 GiB, 0 errors ETA 84:11:57	  

84 Stunden, also rund 2 MB/s. Bis zur Lösung hat es mehrere Runden gebraucht. Weil die einzelnen Schritte für andere vielleicht nützlich sind, schreibe ich sie hier auf.

Runde 1: Ist es der Laptop?

Die naheliegende Vermutung war meine Einstellung IOSchedulingClass=idle im Dienst. Ein ionice und renice auf den laufenden Prozess hat aber kaum etwas gebracht. top zeigte den Laptop zu 99 % im Leerlauf, kein Warten auf die SSD. Und ein dd auf eine große Datei las mit mehreren GB/s. Der Laptop wartet also auf das NAS.

Runde 2: Eine Netzwerkdose mit 100 Mbit

Auf dem NAS lieferte der rest-server lokal 233 MB/s:

  curl -s -u fw13 -o /dev/null -w '%{speed_download}\n' http://127.0.0.1:8000/fw13/data/<Datei-ID>	  

Vom Laptop aus, egal ob über Tailscale oder direkt im LAN, kamen nur etwa 11 MB/s an. Das ist ziemlich genau die Grenze einer 100-Mbit-Verbindung. Dabei zeigten beide Netzwerkkarten 1000 Mbit (cat /sys/class/net/<if>/speed). Der Engpass lag also dazwischen, in meinem Fall in der Hausverkabelung. Mit einer anderen Netzwerkdose kamen über Tailscale plötzlich 75 MB/s an.

Problem gelöst, dachte ich.

Runde 3: Hochladen ist nicht Herunterladen

Nach einigen Minuten lag restic aber wieder bei nur 3 MB/s. Mein Test hatte nur die Richtung NAS → Laptop gemessen, das Backup schreibt aber in die andere Richtung. Also alles einzeln durchgemessen:

Test Ergebnis
Schreiben auf die NAS-Platten (dd, 2 GB) 128 MB/s
Netz Laptop → NAS (iperf3) ca. 500 Mbit/s (≈ 60 MB/s)
fsync für ein 16-MB-Paket am NAS ca. 0,3 s
restic mit 20 statt 5 Verbindungen kein Unterschied

Alles einzeln schnell genug, und trotzdem kamen über Tailscale nur 3 MB/s an. Auffällig war, dass der Tailscale-Container auf dem NAS dabei 25 bis 30 % CPU brauchte.

Zwei Dinge sind mir in den Ausgaben von iperf3 aufgefallen. Erstens war die Absenderadresse meines Laptops eine Tailscale-Adresse (100.x.x.x), obwohl ich die LAN-Adresse des NAS angesprochen hatte. Der Grund: Mein Home Assistant bietet im Tailnet eine Subnetz-Route ins Heimnetz an, und der Laptop hat sie übernommen. Selbst der „direkte“ Verkehr im LAN lief also verschlüsselt durch Tailscale. Abhilfe am Laptop:

  sudo tailscale set --accept-routes=false
  ip route get 192.168.1.10    # sollte jetzt die LAN-Schnittstelle zeigen	  

Zweitens fehlte noch genau ein Test: das Hochladen durch tailscale serve. Also den rest-server-Port testweise im LAN geöffnet ("8000:8000" in der Compose-Datei), die Repository-Adresse am Laptop auf

 rest:http://192.168.1.10:8000/fw13/	  

umgestellt und neu gestartet:

[4:00] 7.09%  37763 files 28.291 GiB, total 276774 files 398.835 GiB, 0 errors ETA 1:48:29
[5:01] 8.05%  39008 files 32.113 GiB, total 276774 files 398.835 GiB, 0 errors ETA 1:45:58	  

Rund 3,7 GiB pro Minute, also etwa 60 MB/s, statt vorher 0,19. Der Upload durch den Proxy von tailscale serve auf dem NAS war die Bremse. Beim Herunterladen ist er schnell, beim Hochladen der vielen 16-MB-Pakete von restic aber nicht.

Die Lösung: zwei Wege

Daher der zweite Weg im Skript: Zuhause geht restic direkt per HTTP an den rest-server, unterwegs weiter über Tailscale. Unterwegs sind die täglichen Änderungen klein, da reichen die 3 MB/s. Damit das Skript nicht in einem fremden Netz mit zufällig gleicher Adresse das Passwort verschickt, prüft es vorher die MAC-Adresse des NAS.

Ein Kompromiss bleibt: Im Heimnetz geht das Passwort für den rest-server unverschlüsselt über HTTP. Die Backup-Daten selbst verschlüsselt restic immer, und löschen kann man wegen append-only ohnehin nichts. Für ein Heimnetz halte ich das für vertretbar.

Mit accept-routes=false erreiche ich Geräte im Heimnetz unterwegs nur noch über ihre Tailscale-Namen, nicht mehr über 192.168.1.x. Für mich passt das.

Der erste Home-Lauf mit knapp 399 GiB dauerte danach etwa zwei Stunden, der erste System-Lauf mit 32 GiB nur sechseinhalb Minuten. Gespeichert wurden davon dank Kompression 16,8 GiB.

Fazit

Das Grundgerüst aus restic, rest-server und zwei systemd-Timern war schnell aufgebaut und läuft ab jetzt ohne mein Zutun. Den meisten Aufwand hat die Ursachenforschung für die langsame Übertragungsgeschwindigkeit gemacht. Dabei habe ich gelernt, Up- und Download getrennt zu messen und jedes Stück der Kette einzeln zu prüfen, statt beim ersten guten Messwert aufzuhören.

Und das wichtigste: Ich habe jetzt endlich ein Backup vom Laptop, das ich auch schon einmal zurückgespielt habe.

Kommentiere via unter dem Hashtag #20261025_ResticNotebookBackupNas (verteilt) oder via E-Mail (langlebig).