April 2026:
Nachdem ich etwas mehr als 1 Jahr Docker unter macOS 13 verwendet habe und mit allen möglichen Hürden zu kämpfen hatte, wurde auf Debian 12, ein reines minimalistisch installiertes Linux, migriert. Ich denke, ich kann zufrieden sein.
Das System ist auf hohe Verfügbarkeit der Daten und maximale Stromersparnis im 24/7-Betrieb ausgelegt.

Speicher-Architektur (LVM Mirroring)
Um Ausfallsicherheit zu gewährleisten, wurde konsequent auf LVM-Mirroring gesetzt. Dies ermöglicht den Austausch einer defekten SSD im laufenden Betrieb ohne Datenverlust.
Volume Group
system: Spiegelt das Betriebssystem (Debian 12).Volume Group
docker: Trennung von OS und Anwendungsdaten. Alle Docker-Volumes und Container-Configs liegen hier.Vorteil: Sollte eine SSD sterben, läuft der Server ohne Unterbrechung weiter.
Mein Container:

Frigate – Kamera Überwachungssystem
Bei mir im Garten habe ich 3 Reolink CX410 Kameras mit je 128GB SanDisk High Endurance microSDXC. Zu Hause läuft Frigate als Docker container mit einer weiteren Kamera eine RLC-810A. Damit Frigate mit den Kameras im Garten kommunizieren kann, besteht eine Internet Wireguard VPN LAN to LAN Kopplung. Zu Hause habe ich DSL und im Garten Mobile Daten via 4G oder 5G. Ordentlich funktionieren kann das Ganze Konstrukt nur mit einem seeed studio Google Coral USB Accelerator, der am USB 3.0 hängt. Ansonsten müssten die Berechnungen/Auswertungen der Bilder die CPU erledigen, die dann mit ~80-90% Dauerlast bedeuten würde. Anstatt ~22W low power System wären das ohne den Coral USB Accelerator dann locker ~60W Last, was natürlich gar nicht geht. Für die Speicherung der Kamera Media Daten ist eine dedizierte SATA 2,5″ Disk zuständig.

Die Backup-Strategie (3-Stufen-Konzept)
Die Sicherung erfolgt vollautomatisch über das Tool Restic, orchestriert durch Systemd-Timer auf dem Host und visualisiert durch Backrest im Docker-Container.
Stufe 1: Lokale SSD-Sicherung (6-stündlich)
Ziel: 500 GB USB-SSD (
/mnt/restic-usb)Intervall: Alle 6 Stunden via Systemd-Timer.
Besonderheit: Extrem schnelle Wiederherstellung von versehentlich gelöschten Dateien.
Stufe 2: NAS-Sicherung (Nächtlich)
Ziel: Synology NAS via NFS/SMB (
/mnt/backup)- Intervall: Alle 24 Stunden, 3 Uhr nachts, via Systemd-Timer.
Workflow: Das System weckt das NAS per Wake-on-LAN (WOL), mountet das Dateisystem, führt den Restic-Snapshot aus und fährt das NAS danach wieder herunter. Sollte das Synology NAS bereits eingeschaltet gewesen sein, wird es natürlich nicht heruntergefahren.
Effizienz: Das NAS verbraucht nur Strom, wenn es wirklich benötigt wird.
Stufe 3: Monitoring & GUI
Tool: Backrest (Web-Interface für Restic)
Funktion: Visualisierung aller Snapshots, einfaches Browsen in Backups und schneller Datei-Restore ohne Kommandozeile.

Hier mein docker-compose für Backrest (Restic), die nur so richtig startet, wenn das NAS offline ist.
services:
backrest:
image: garethgeorge/backrest:latest
container_name: backrest
user: "0:0"
restart: unless-stopped
ports:
- 9898:9898
environment:
- BACKREST_CONFIG=/config/config.json
- BACKREST_DATA=/data
- XDG_CACHE_HOME=/data/cache
volumes:
- /srv/docker/active/backrest/config:/config
- /srv/docker/active/backrest/data:/data
# USB-Laufwerk
- /mnt/restic-usb/repo:/repos/usb
# SSH-Key für das NAS
- /root/.ssh/id_ed25519:/id_ed25519:ro
# Die neue SSH-Config (Alias-Definition)
- /srv/docker/active/backrest/config/ssh_config:/root/.ssh/config:ro
# Passwörter & Env-Dateien
- /root/.restic/password-usb:/passwords/usb-pass:ro
- /etc/restic/env:/passwords/nas-env:roWichtig: Der Zugriff auf’s Repo vom NAS erfolgt dann auch via SFTP Protokoll!

The Ultimate Persistent NAS-Backup Orchestrator
/usr/local/bin/restic-backup-all.sh
#!/bin/bash
set -euo pipefail
source /etc/restic/env
# --- E-Mail Konfiguration ---
EMAIL="chris@redel-ma.de"
LOG="/var/log/restic-backup-all.log"
LOCKFILE="/run/restic-backup-all.lock"
PAPERLESS="/srv/docker/active/paperless/docker-compose.yml"
NEXTCLOUD="/srv/docker/active/nextcloud/docker-compose.yml"
NAS_IP="192.168.1.5"
NAS_USER="credel"
# -------------------------
# NAS STATE (persistent)
# -------------------------
NAS_STATE_FILE="/run/restic-nas-started.lock"
NAS_STARTED_BY_SCRIPT=0
[[ -f "$NAS_STATE_FILE" ]] && NAS_STARTED_BY_SCRIPT=1
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG"
}
fail() {
log "ERROR: $*"
exit 1
}
send_error_mail() {
log "Sende Fehler-Benachrichtigung an $EMAIL..."
# Sendet die letzten 100 Zeilen des Logs, damit du den Grund sofort siehst
{
echo "Restic Backup Fehlermeldung von Host: $(hostname)"
echo "Zeitpunkt: $(date)"
echo "-------------------------------------------"
tail -n 100 "$LOG"
} | mail -s "BACKUP FEHLER (NAS): $(hostname)" "$EMAIL"
}
nas_is_up() {
ssh -o BatchMode=yes \
-o ConnectTimeout=2 \
-o StrictHostKeyChecking=no \
"$NAS_USER@$NAS_IP" "exit" >/dev/null 2>&1
}
start_nas_if_needed() {
log "CHECK NAS STATE"
if nas_is_up; then
log "NAS already running - skip start"
return 0
fi
log "START NAS (WOL)"
echo "1" > "$NAS_STATE_FILE"
NAS_STARTED_BY_SCRIPT=1
/usr/local/bin/syno6x6-start.sh
log "WAIT NAS READY"
for i in {1..90}; do
if nas_is_up; then
log "NAS READY"
log "NAS STORAGE CHECK"
for j in {1..30}; do
if mountpoint -q /mnt/backup && [[ -d /mnt/backup/restic ]]; then
log "NAS STORAGE READY"
break
fi
sleep 2
done
mountpoint -q /mnt/backup || fail "NAS storage not mounted"
[[ -d /mnt/backup/restic ]] || fail "RESTIC repo missing"
return 0
fi
sleep 2
done
fail "NAS not reachable after startup"
}
stop_nas_if_started_by_script() {
if [[ "${NAS_STARTED_BY_SCRIPT:-0}" -ne 1 ]]; then
log "NAS was already running → skip shutdown"
return 0
fi
log "STOP NAS"
ssh "$NAS_USER@$NAS_IP" \
"echo '**************************' | sudo -S /usr/syno/sbin/synoshutdown --shutdown" \
>/dev/null 2>&1 || log "NAS shutdown failed or already offline"
rm -f "$NAS_STATE_FILE"
}
cleanup() {
# Fehlerprüfung: $? fängt den Status des Skripts vor dem Eintritt in cleanup ab
local exit_code=$?
if [ $exit_code -ne 0 ]; then
log "Skript mit Fehler abgebrochen (Code: $exit_code)."
send_error_mail
fi
log "START containers (paperless + nextcloud)"
docker compose -f "$PAPERLESS" start || true
docker compose -f "$NEXTCLOUD" start || true
sleep 10
log "HEALTH CHECK"
docker ps --format '{{.Names}} {{.Status}}' | tee -a "$LOG"
stop_nas_if_started_by_script
rm -f "$LOCKFILE"
rm -f /tmp/docker-containers.txt /tmp/docker-*.json
}
# Der Trap sorgt dafür, dass cleanup IMMER ausgeführt wird
trap cleanup EXIT
# -------------------------
# LOCK
# -------------------------
if [[ -e "$LOCKFILE" ]]; then
fail "Backup already running"
fi
touch "$LOCKFILE"
log "BACKUP START"
# -------------------------
# NAS HANDLING
# -------------------------
start_nas_if_needed
# -------------------------
# PRECHECKS
# -------------------------
docker info >/dev/null 2>&1 || fail "Docker not running"
restic snapshots >/dev/null 2>&1 || fail "Restic repo not reachable"
# -------------------------
# STOP STACKS (DETERMINISTIC FAST MODE)
# -------------------------
log "STOP containers (paperless + nextcloud)"
docker compose -f "$PAPERLESS" stop --timeout 30 || {
log "WARNING: Paperless graceful stop failed → forcing"
docker compose -f "$PAPERLESS" kill || true
}
docker compose -f "$NEXTCLOUD" stop --timeout 30 || {
log "WARNING: Nextcloud graceful stop failed → forcing"
docker compose -f "$NEXTCLOUD" kill || true
}
sleep 5
for c in nc_cron nc_app nc_redis nc_db; do
if docker ps --format '{{.Names}}' | grep -q "^${c}$"; then
log "FORCE STOP (stubborn container): $c"
docker stop --time=20 "$c" || docker kill "$c" || true
fi
done
sleep 3
log "STOP VERIFY"
# -------------------------
# PREPARE METADATA (All containers)
# -------------------------
log "DOCKER CONTAINER EXPORT (METADATA)"
docker ps -a --format '{{.Names}}' > /tmp/docker-containers.txt
for c in $(cat /tmp/docker-containers.txt); do
docker inspect "$c" > "/tmp/docker-$c.json"
done
# -------------------------
# BACKUP CORE & METADATA
# -------------------------
log "RESTIC BACKUP (Full /srv/docker/active/ + System)"
restic backup \
/etc \
/root \
/home \
/usr/local \
/srv/docker/active \
/tmp/docker-containers.txt \
/tmp/docker-*.json \
--tag "full-backup" \
--exclude "/srv/docker/active/frigate/media" \
--exclude "/srv/docker/active/frigate/recordings" \
--exclude "/srv/docker/active/frigate/clips" \
--exclude "/srv/docker/*/tmp"
# -------------------------
# INTEGRITY CHECK
# -------------------------
log "RESTIC CHECK (Verify Repository)"
restic check || fail "Restic repository check failed!"
# -------------------------
# RETENTION
# -------------------------
log "RETENTION POLICY"
restic forget \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 6 \
--tag "full-backup" \
--prune
log "BACKUP DONE"
/usr/local/bin/restic-backup-usb.sh
#!/bin/bash
# set -e sorgt dafür, dass das Skript bei Fehlern sofort stoppt
set -euo pipefail
# --- Konfiguration ---
export RESTIC_REPOSITORY=/mnt/restic-usb/repo
export RESTIC_PASSWORD_FILE=/root/.restic/password-usb
export HOME=/root
export XDG_CACHE_HOME=/root/.cache
LOGTAG="[RESTIC-USB]"
EMAIL="chris@redel-ma.de"
LOGFILE="/tmp/restic-usb-status.log"
# --- Fehler-Funktion ---
# Diese Funktion wird aufgerufen, wenn irgendwo im Skript ein Fehler passiert
failure_handler() {
echo "$LOGTAG !!! FEHLER ENTDECKT !!!" >> "$LOGFILE"
mail -s "BACKUP FEHLER: $(hostname) - USB" "$EMAIL" < "$LOGFILE"
}
# Hier sagen wir dem System: "Wenn ein Fehler (ERR) auftritt, führe failure_handler aus"
trap 'failure_handler' ERR
# --- Start der Aufzeichnung ---
# Ab hier schreiben wir alles, was passiert, in die Logdatei
exec > >(tee "$LOGFILE") 2>&1
echo "$LOGTAG START $(date)"
# 1. Erweiterter Safety check
if ! mountpoint -q /mnt/restic-usb; then
echo "$LOGTAG ERROR: USB-Laufwerk ist nicht unter /mnt/restic-usb gemountet!"
# Wir lösen manuell einen Fehler aus, damit der Trap anspringt
false
fi
# Prüfen, ob das Repository-Verzeichnis existiert
if [ ! -d "$RESTIC_REPOSITORY" ]; then
echo "$LOGTAG ERROR: Restic Repository Verzeichnis fehlt!"
false
fi
# 2. Docker Metadaten
echo "$LOGTAG EXPORT METADATA"
mkdir -p /tmp/docker-backup-metadata
docker ps -a --format '{{.Names}}' > /tmp/docker-backup-metadata/containers.txt
while read -r name; do
docker inspect "$name" > "/tmp/docker-backup-metadata/$name.json"
done < /tmp/docker-backup-metadata/containers.txt
# 3. Live-Backup
echo "$LOGTAG RUN BACKUP (LIVE MODE)"
restic backup \
/etc /home /root /srv/docker/active /usr/local /tmp/docker-backup-metadata/ \
--tag "usb-full-backup" \
--one-file-system \
--exclude "/srv/docker/active/frigate/media" \
--exclude "/srv/docker/active/frigate/recordings" \
--exclude "/srv/docker/active/frigate/clips" \
--exclude "**/tmp" --exclude "**/.cache"
# 4. Retention & Integrität
echo "$LOGTAG RETENTION POLICY"
restic forget --keep-daily 30 --keep-weekly 12 --keep-monthly 12 --tag "usb-full-backup" --prune
if [ $(( ( RANDOM % 10 ) )) -eq 0 ]; then
echo "$LOGTAG RANDOM CHECK"
restic check
fi
# 5. Aufräumen & Sync
rm -rf /tmp/docker-backup-metadata/
echo "$LOGTAG FLUSHING FILESYSTEM BUFFERS"
sync
echo "$LOGTAG DONE $(date)"
# Optional: Erfolgsmail (kannst du löschen, wenn es nervt)
# echo "Backup auf USB war erfolgreich." | mail -s "Backup OK: $(hostname)" "$EMAIL"
Monitoring:
Es gibt verschiedene Möglichkeiten das System zu Monitoren. Uptime Kuma als Container erscheint mir die beste Methode zu sein um diverse Ressourcen zu Monitoren und im Fall eines Problems eine Nachricht zugestellt zu bekommen.

CTOP darf auch nicht fehlen.
Glances schaded auch nicht, wenn’s darum geht eine komplette Systemübersicht zu bekommen:

Zum Schluss noch:
Das „Efficiency-Tuning“ Logbuch
Die Fehlentscheidung: i5-9500 & B360M
Versprechen: 15W Idle.
Realität: ~20W Idle.
Lerneffekt: Neuere Consumer-Hardware ist nicht automatisch effizienter als optimierte Server-Hardware, wenn man die restlichen Komponenten (LVM, mehrere SSDs) einbezieht.
Energetischer Kontext (Balkonkraftwerk)
PV-Anlage: 2000W Peak / 6,3kWh Akku.
Grundlast Nacht: 75W gesamt.
Server-Anteil: 21W (~28%).
Fazit: Dank des großen Speichers wird der Server zu 100% autark durch Sonnenenergie betrieben, selbst in bewölkten Phasen reicht die Kapazität locker aus.
Ganz zum Schluss noch:
Dieses Setup hat mich nicht Stunden, es hat etliche Tage gebraucht um hier endlich an diesem für mich finalen Punkt anzukommen. Sicherlich wird hier und da noch zu optimieren sein. Vor allem muss ein (auch Desaster) Recovery simuliert und dokumentiert werden.
Update 13.05.2026:
Ich hatte zuvor ein ein etwas älteres System mit Intel Server 1150 board und i5-5675C CPU. Das system lief super mit 21W – OHNE Frigate. Mit 4 Kameras und Frigate waren trotz Coral alle 4 Kerne mit ~50% load busy und meine schönen 21W waren futsch, weswegen auf dieses System gewechselt wurde. Das ist aber aus meinem Media PC „entwendet“ worden und soll da auch wieder rein, weswegen für ~100€ in der Bucht ein FUJITSU Desktop ESPRIMO D738 angeschafft wurde. Es wird etwas eng zugehen im Gehäuse, denke aber gut meine 5x SATA SSD’s dort unterbringen zu können. Ich strebe mit dem System deutlich <20W an und Berichte dazu.
#
