24W idle Low-Power Home-Docker-Server (Debian 12)

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.

KomponenteDetails
MainboardIntel S1200V3RP (LGA1150, Server-Chipsatz)
CPUIntel Core i5-5675C (4 Kerne, Broadwell, 65W TDP)
RAM16 GB DDR3L (4x 4GB)
OS-Storage2x 240 GB Sandisk SSD (LVM Mirror / RAID 1)
Docker-Storage2x 480 GB Intel 530 series SSD (LVM Mirror / RAID 1)
Frigate-Storage500GB SATA intern 2,5″ WD black
Backup-Storage500 GB SSD (Extern via USB) + DS3622XS+ 30TB NAS
Leistungsaufnahme~24W Idle (gemessen an der Dose)

Der i5-5675C Vorteil: > „Warum diese spezielle CPU? Dank des integrierten 128MB eDRAM (L4-Cache) bietet dieser Broadwell-Chip eine Effizienz, die viele Nachfolgegenerationen im Idle-Bereich erst viel später erreichten. Gepaart mit dem S1200V3RP Server-Board ergibt das eine stabile Basis mit IPMI-Support bei nur 21W Leistungsaufnahme – ideal für einen 24/7 Home-Server mit anspruchsvoller Docker-Umgebung.“

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. Damit Frigate mit den Kameras kommunizieren kann, besteht eine Internet 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 ~25W 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:ro

Wichtig: 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

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
}

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() {
  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
}

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"

# -------------------------
# 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 -euo pipefail

export RESTIC_REPOSITORY=/mnt/restic-usb/repo
export RESTIC_PASSWORD_FILE=/root/.restic/password-usb

# systemd environment fix
export HOME=/root
export XDG_CACHE_HOME=/root/.cache

LOGTAG="[RESTIC-USB]"

echo "$LOGTAG START $(date)"

# 1. Safety check: USB muss gemountet sein
if ! mountpoint -q /mnt/restic-usb; then
  echo "$LOGTAG ERROR: USB nicht gemountet - Abbruch"
  exit 1
fi

# 2. Docker Metadaten (Sichert die Struktur, während die Container laufen)
echo "$LOGTAG 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

# 3. Live-Backup (Ohne Stopp, mit Frigate-Filter)
echo "$LOGTAG RUN BACKUP (LIVE MODE)"
restic backup \
  /etc \
  /home \
  /root \
  /srv/docker/active \
  /usr/local \
  /tmp/docker-containers.txt \
  /tmp/docker-*.json \
  --tag "usb-full-backup" \
  --exclude "/srv/docker/active/frigate/media" \
  --exclude "/srv/docker/active/frigate/recordings" \
  --exclude "/srv/docker/active/frigate/clips" \
  --exclude "/srv/docker/*/tmp"

# 4. Retention: Deine 1-Jahres-Policy
echo "$LOGTAG RETENTION POLICY"
restic forget \
  --keep-daily 30 \
  --keep-weekly 12 \
  --keep-monthly 12 \
  --tag "usb-full-backup" \
  --prune

# 5. Aufräumen der temporären Dateien
rm -f /tmp/docker-containers.txt /tmp/docker-*.json

echo "$LOGTAG DONE $(date)"

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.

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.

#