Leserartikel [QNAP TS-453D] QTS adee OnlyContainers juhee

###Zaunpfahl###

Lt. Commander
Registriert
Jan. 2010
Beiträge
1.685
Vorwort: Der folgende Artikel wurde überwiegend mithilfe von KI (Gemini 3.8 Thinking) erstellt. Dennoch entspricht er meiner Erfahrung, Absicht und Meinung.

Flatcar Container Linux auf dem QNAP TS-453D: Das unzerstörbare KISS-NAS​


1. Einleitung, Motivation & Philosophie​


Fertig-NAS-Systeme von QNAP glänzen ab Werk mit zahllosen Zusatzanwendungen, Multimedia-Paketen und bunten Web-Oberflächen. Wer sein NAS im privaten Homelab jedoch primär als verlässliches Datengrab, für Backups und als Docker-Host betreibt, zahlt für diesen Funktionsumfang einen hohen Preis: Hohe Grundlast, Telemetrie, proprietäre Software-Schichten und in der Vergangenheit immer wieder kritische Sicherheitslücken (wie Qlocker oder DeadBolt).

Hauptsächlich hat mich das OS und die am Ende doch halbgare Software mit ihren Eigenheiten und Limitierungen gestört.

Die Antwort auf dieses Problem lautet KISS (Keep It Simple, Stupid) in Verbindung mit einem immutablen (unveränderlichen) Betriebssystem: Flatcar Container Linux.

Die Kernideen hinter diesem Aufbau:

  • Möglichst alles in Container: Einerseits kenn ich mich damit schon aus und im schlimmsten Fall lerne ich was hinzu. Dazu ist alles sehr schnell reproduzierbar. Also die ganze Software mit all seinen Einstellungen (copy&paste). Und zum großen Teil Hardware unabhängig. Also wenn man mal wechseln wollen würde. Ein paar Hardware Konfigs anpassen und das kann man dann auch der KI überlassen wenn man möchte, ist ja alles nur Text.
  • Unveränderliches Basissystem: Das Root-Dateisystem ist schreibgeschützt montiert. Betriebssystem-Updates erfolgen atomar über zwei getrennte Partitionen (A/B-Schema) im Hintergrund. Schlägt ein Update fehl, startet der Host automatisch in die funktionierende Version zurück.
  • Kein Konfigurationsdrift: Es gibt keinen klassischen Paketmanager (apt, yum), der über Jahre fragmentieren oder zerschossen werden kann. Die gesamte Host-Konfiguration wird rein deklarativ über eine einzige Datei (Ignition/Butane) definiert. Das NAS ist in unter fünf Minuten auf jeder beliebigen x86-Hardware exakt identisch neu aufgesetzt.
  • Portabilität schlägt proprietäre Storage-Pools: Unter QTS liegen Daten oft in proprietären LVM- und Storage-Pool-Strukturen. Stirbt das QNAP-Mainboard, lassen sich die Platten an einem normalen PC kaum oder nur mit extremem Aufwand auslesen. In diesem Setup läuft jedes Laufwerk als eigenständiges Btrfs-Dateisystem. Geht die Hardware kaputt, zieht man die Festplatte heraus, steckt sie per SATA oder USB an jedes beliebige Linux-Notebook und hat sofort vollen Datenzugriff.
  • Backups vor Redundanz: Im privaten Umfeld wird Ausfallsicherheit (RAID-Parität) oft mit Datensicherheit verwechselt. Ein RAID schützt lediglich vor Hardware-Ausfallzeiten, nicht vor versehentlichem Löschen, Dateisystemfehlern oder Ransomware. Die Devise lautet hier: Keine unnötige RAID-Komplexität, sondern strikte 3-2-1-Snapshot-Backups via Kopia auf getrennte interne und externe Datenträger.






2. Hardware-Basis & Vorbereitung​


Das QNAP TS-453D bietet eine hervorragende Hardware-Grundlage für ein schlankes Linux-System:

  • CPU: Intel Celeron J4125 (4 Kerne, 2,0 bis 2,7 GHz, x86_64, volles UEFI)
  • Netzwerk: 2x 2,5 GbE (Intel I225-V)
  • Video/I/O: HDMI-Port und USB-Ports für Tastatur und Konsolenausgabe

Laufwerksaufteilung in den 4 Schächten:
  1. Schacht 1: 250 GB SATA-SSD für das Flatcar-Basissystem und Root-Btrfs
  2. Schacht 2: 250 GB SATA-SSD (toshiba_ssd) für Arbeitsdaten, Datenbanken und Anwendungs-Caches
  3. Schacht 3: 8 TB HDD (main_hdd_8tb) für die eigentlichen Hauptdaten (Medien, Dokumente, Fotos)
  4. Schacht 4: 3 TB HDD (backup_hdd_3tb) als lokales Ziel für versionierte Kopia-Snapshots


Wichtig: Schutz des internen QNAP-DOM
Auf dem Mainboard des TS-453D sitzt ein fest verlöteter USB-DOM-Flashspeicher (4 GB) mit dem werksseitigen QNAP-Bootloader. Dieser Flash-Speicher wird bei der Installation nicht überschrieben oder angefasst. Flatcar wird vollständig auf der 250-GB-SSD in Schacht 1 installiert. Wird die SSD entfernt, bootet das Gerät wie am ersten Tag in den QNAP-Einrichtungsassistenten – der Wiederverkaufswert und die Reversibilität bleiben zu 100 % erhalten.

Außerdem würden sich die 4 GB nicht lohnen darauf das OS zu installieren. Meiner Meinung nach.

Vorbereitung im BIOS:
  1. Monitor per HDMI und Tastatur per USB an das TS-453D anschließen.
  2. Beim Einschalten wiederholt [DEL] bzw. [F7] drücken, um das UEFI-BIOS aufzurufen.
  3. Die Boot-Reihenfolge so anpassen, dass die SATA-SSD vor dem internen QNAP-Flash-Speicher bootet.



3. Deklarative Bereitstellung via Butane & Ignition​


Flatcar wird beim allerersten Bootvorgang über sogenannte Ignition-Dateien (JSON) konfiguriert. Da JSON von Hand fehleranfällig ist, verwendet man das Übersetzungswerkzeug Butane und formuliert das System deklarativ in YAML (butane.yaml).

Installation:
https://www.flatcar.org/docs/latest/deploy/bare-metal/installing-to-disk/
Das ist erst etwas ungewöhnlich aber ich finds am Ende ganz geil.
Man bootet auf dem System in irgendein Linux seiner wahl. Erstellt irgendwo die ignition.json aus der butane.yaml.
Führt den Befehl aus für das entsprechende auserwählte OS Laufwerk.
Ggf. Bootreihenfolge anpassen und rein booten. Per ssh drauf und loslegen.

Code:
variant: flatcar
version: 1.0.0

passwd:
  users:
    # 1. Admin-Benutzer (Passwort deaktiviert, reiner SSH-Key-Login)
    - name: admin
      groups:
        - docker
        - sudo
      ssh_authorized_keys:
        - "ssh-ed25519 AAAAC3Nz..."
    # 2. Eingeschränkter Shutdown-Benutzer für Heimautomation
    - name: shutdownuser
      password_hash: "$6$saltsalt$hashgoeshere..."

storage:
  directories:
    - path: /mnt/usb
      mode: 0755
  filesystems:
    # Host-SSD: Stateful-Root-Partition abfangen und als Btrfs formatieren
    - device: /dev/disk/by-label/ROOT
      format: btrfs
      wipe_filesystem: true
      label: ROOT

  files:
    # Automatische Reboots nach OS-Updates deaktivieren
    - path: /etc/flatcar/update.conf
      overwrite: true
      contents:
        inline: |
          REBOOT_STRATEGY=off
    # Wake-on-LAN für alle Interfaces aktivieren
    - path: /etc/systemd/network/50-wol.link
      mode: 0644
      contents:
        inline: |
          [Match]
          OriginalName=*
          [Link]
          WakeOnLan=magic
    # Sudoers-Berechtigung für den Shutdown-Nutzer
    - path: /etc/sudoers.d/shutdownuser
      mode: 0440
      contents:
        inline: |
          shutdownuser ALL=(ALL) NOPASSWD: /usr/bin/systemctl poweroff, /usr/bin/systemctl reboot, /usr/bin/whoami
    # SSH-Einschränkung via ForceCommand
    - path: /etc/ssh/sshd_config.d/shutdown.conf
      mode: 0644
      contents:
        inline: |
          Match User shutdownuser
              PasswordAuthentication yes
              ForceCommand /bin/sh -c 'case "$SSH_ORIGINAL_COMMAND" in *whoami*) sudo /usr/bin/whoami ;; *poweroff*|*shutdown*) sudo /usr/bin/systemctl poweroff ;; *reboot*) sudo /usr/bin/systemctl reboot ;; *) echo "Access denied"; exit 1 ;; esac'
    # Skript für automatisches USB-Backup
    - path: /root/run-backup.sh
      mode: 0755
      contents:
        inline: |
          #!/bin/bash

          CONTAINER_NAME="kopia"
          USB_DESTINATION="/mnt/usb/kopia-offline-sync"

          echo "$(date): USB backup triggered."

          # 1. Zielverzeichnis anlegen und Rechte setzen
          mkdir -p "$USB_DESTINATION"
          chown -R 1000:1000 "$USB_DESTINATION"

          # 2. Snapshot-Sync aus Kopia anstoßen
          docker exec $CONTAINER_NAME kopia repository sync-to filesystem --path "$USB_DESTINATION" || true

          # 3. Puffer auf Platte schreiben
          sync

          # 4. Externe Festplatte sicher aushängen
          systemctl stop mnt-usb.mount

          echo "$(date): Backup finished and drive unmounted."

systemd:
  units:
    # Docker erst starten, wenn alle Speicher-Pools gemountet sind
    - name: docker.service
      enabled: true
      dropins:
        - name: wait-for-mounts.conf
          contents: |
            [Unit]
            After=mnt-toshiba_ssd.mount mnt-main_hdd_8tb.mount mnt-backup_hdd_3tb.mount
            Requires=mnt-toshiba_ssd.mount mnt-main_hdd_8tb.mount mnt-backup_hdd_3tb.mount

    # Btrfs-Mount: Cache- und App-SSD
    - name: mnt-toshiba_ssd.mount
      enabled: true
      contents: |
        [Unit]
        Description=Mount Toshiba SSD
        After=local-fs.target
        [Mount]
        What=/dev/disk/by-label/toshiba_ssd
        Where=/mnt/toshiba_ssd
        Type=btrfs
        Options=defaults,compress=zstd:1
        [Install]
        WantedBy=multi-user.target

    # Btrfs-Mount: 8 TB Hauptdatenplatte
    - name: mnt-main_hdd_8tb.mount
      enabled: true
      contents: |
        [Unit]
        Description=Mount Main 8TB HDD
        After=local-fs.target
        [Mount]
        What=/dev/disk/by-label/main_hdd_8tb
        Where=/mnt/main_hdd_8tb
        Type=btrfs
        Options=defaults,compress=zstd:1
        [Install]
        WantedBy=multi-user.target

    # Btrfs-Mount: 3 TB interne Backup-Platte
    - name: mnt-backup_hdd_3tb.mount
      enabled: true
      contents: |
        [Unit]
        Description=Mount Backup 3TB HDD
        After=local-fs.target
        [Mount]
        What=/dev/disk/by-label/backup_hdd_3tb
        Where=/mnt/backup_hdd_3tb
        Type=btrfs
        Options=defaults,compress=zstd:1
        [Install]
        WantedBy=multi-user.target

    # Automount für externe USB-Festplatte mit Label "BACKUP"
    - name: mnt-usb.mount
      enabled: true
      contents: |
        [Unit]
        Description=Automount USB Drive by Label
        BindsTo=dev-disk-by\x2dlabel-BACKUP.device
        After=dev-disk-by\x2dlabel-BACKUP.device

        [Mount]
        What=/dev/disk/by-label/BACKUP
        Where=/mnt/usb
        Type=btrfs
        Options=defaults,noatime

        [Install]
        WantedBy=dev-disk-by\x2dlabel-BACKUP.device

    # Automatischer Backup-Hook beim Einhängen der USB-Platte
    - name: usb-hook.service
      enabled: true
      contents: |
        [Unit]
        Description=Trigger Kopia Backup on USB Mount
        Requires=mnt-usb.mount docker.service
        After=mnt-usb.mount docker.service

        [Service]
        Type=oneshot
        TimeoutStartSec=infinity
        ExecStart=/bin/bash /root/run-backup.sh

        [Install]
        WantedBy=mnt-usb.mount

Die technischen Besonderheiten der Konfiguration:

  1. Btrfs mit transparenter ZSTD-Kompression: Alle internen Datenträger werden als eigenständige Btrfs-Laufwerke mit dem Mount-Parameter compress=zstd:1 betrieben. Das spart Speicherplatz, schont die Schreibzyklen der SSDs und erhöht den sequenziellen Durchsatz komprimierbarer Daten.
  2. Zuverlässige Startreihenfolge (Race-Condition-Schutz): Docker startet unter Linux sehr früh. Würden Container starten, bevor die Festplatten eingehängt sind, würde Docker die Mount-Pfade als leere Verzeichnisse auf der Boot-SSD anlegen. Durch den systemd Drop-In wait-for-mounts.conf wartet der Docker-Daemon zwingend, bis alle drei Partitionen erfolgreich eingebunden sind.
  3. Gehärteter Shutdown-Benutzer: Um das NAS automatisiert (etwa über ein Home-Assistant-Dashboard oder eine USV) herunterzufahren, muss kein voller Root-SSH-Schlüssel hinterlegt werden. Der Nutzer shutdownuser darf sich per Passwort anmelden, wird jedoch im SSH-Daemon über ForceCommand strikt auf poweroff, reboot und whoami beschränkt – alle anderen Befehle oder interaktive Shells werden abgewiesen.
  4. Vorbereiteter USB-Backup-Hook: Beim Anschließen eines externen Datenträgers mit dem Label BACKUP bindet systemd diesen automatisch nach /mnt/usb ein und triggert den usb-hook.service. Das Skript stößt einen Repository-Sync in Kopia an, flusht alle Datenpuffer (sync) und unmountet die Platte sicher. (Praxis-Hinweis: Diese Automatisierung ist zwar sauber deklariert, funktioniert aber nicht, ich machs einfach händisch 🤷‍♂️ )


Standardmäßig hat Flatcar Container Linux kein docker compose.
Code:
mkdir -p ~/.docker/cli-plugins
curl -SL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose



4. Das QNAP-Hardware-Problem: Lüftersteuerung & Kernel-Module​


Auf einer herkömmlichen Linux-Distribution (wie Debian oder Fedora) lässt sich zusätzliche Hardware meist über Paketmanager oder DKMS nachrüsten. Unter Flatcar existieren weder Compiler noch Header im Systemabbild.

Das QNAP TS-453D verwendet zur Lüftersteuerung einen ITE IT8528 Embedded Controller (EC). Ohne das Out-of-Tree-Kernelmodul qnap8528 sowie den im Standard-Flatcar-Kernel deaktivierten Treiber drivetemp kann Linux weder den Gehäuselüfter regeln noch Festplattentemperaturen über das Standard-Subsystem hwmon auslesen.

Bauen im ephemeren Developer-Container

Flatcar stellt für jeden Release-Kanal ein passendes, signiertes Entwickler-Abbild bereit. Das Skript build-drivers.sh lädt dieses Image passend zur exakten Host-Version (/etc/os-release) herunter, validiert die GPG-Signatur und startet den Build isoliert via systemd-nspawn.

Die KI hat mir hier auch die Kommentare ins deutsche übersetzt 😅, egal was solls 🤷‍♂️
Bash:
#!/bin/bash
set -e

# 1. Absoluten Pfad ermitteln und Modulverzeichnis anlegen
BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DIR="$BASE_DIR/module"
mkdir -p "$DIR"
cd "$DIR"

# 2. Exakte LTS-Release-Version ermitteln
source /etc/os-release
URL="https://lts.release.flatcar-linux.net/amd64-usr/${VERSION_ID}/flatcar_developer_container.bin.bz2"
VER_FILE=".version"

# 3. Bereinigung: Veraltete Images nach OS-Updates löschen
if [ -f flatcar_developer_container.bin ]; then
    if [ ! -f "$VER_FILE" ] || [ "$(cat "$VER_FILE")" != "$VERSION_ID" ]; then
        echo "🔄 OS-Update erkannt! Lösche veralteten 1-GB-Container..."
        rm -f flatcar_developer_container.bin "$VER_FILE"
    else
        echo "✅ Developer-Container passt zum aktuellen OS ($VERSION_ID)."
    fi
fi

# 4. Download und Signatur-Verifikation
if [ ! -f flatcar_developer_container.bin ]; then
    rm -rf flatcar_developer_container.bin.bz2* Flatcar_Image_Signing_Key.asc .gnupg_temp
 
    echo "Lade Developer-Container für Flatcar LTS ${VERSION_ID}..."
    curl -f -L --progress-bar "$URL" -o flatcar_developer_container.bin.bz2
 
    echo "Lade GPG-Signatur und Flatcar-Schlüssel..."
    curl -f -L -s "${URL}.sig" -o flatcar_developer_container.bin.bz2.sig
    curl -f -L -s "https://www.flatcar.org/security/image-signing-key/Flatcar_Image_Signing_Key.asc" -o Flatcar_Image_Signing_Key.asc
 
    export GNUPGHOME="$(pwd)/.gnupg_temp"
    mkdir -p "$GNUPGHOME"
    chmod 700 "$GNUPGHOME"
 
    gpg --quiet --import Flatcar_Image_Signing_Key.asc
    if ! gpg --verify flatcar_developer_container.bin.bz2.sig flatcar_developer_container.bin.bz2; then
        echo "❌ FEHLER: Signaturprüfung fehlgeschlagen!"
        rm -rf "$GNUPGHOME" flatcar_developer_container.bin.bz2* Flatcar_Image_Signing_Key.asc
        exit 1
    fi
    echo "✅ Signatur verifiziert. Entpacke Image..."
    rm -rf "$GNUPGHOME" Flatcar_Image_Signing_Key.asc flatcar_developer_container.bin.bz2.sig
 
    bunzip2 flatcar_developer_container.bin.bz2
    echo "$VERSION_ID" > "$VER_FILE"
fi

# 5. Build-Skript für den Developer-Container schreiben
cat << 'EOF' > compile.sh
#!/bin/bash
set -e
export FEATURES="-network-sandbox -ipc-sandbox -pid-sandbox"

echo "Hole Kernel-Quellen via Gentoo Portage..."
emerge-gitclone -q
emerge -gKv coreos-sources
zcat /proc/config.gz > /usr/src/linux/.config

# drivetemp in Kernelkonfiguration aktivieren
sed -i 's/# CONFIG_SENSORS_DRIVETEMP is not set/CONFIG_SENSORS_DRIVETEMP=m/' /usr/src/linux/.config

make -C /usr/src/linux olddefconfig
make -C /usr/src/linux modules_prepare

# Natives drivetemp-Modul kompilieren
echo "Kompiliere drivetemp..."
make -C /usr/src/linux M=drivers/hwmon KBUILD_MODPOST_WARN=1 modules
cp /usr/src/linux/drivers/hwmon/drivetemp.ko /host/

# qnap8528 EC-Treiber klonen und kompilieren
echo "Kompiliere qnap8528..."
cd /tmp
rm -rf qnap8528
git clone -q https://github.com/0xGiddi/qnap8528.git
cd qnap8528/src
make -C /usr/src/linux M=$(pwd) KBUILD_MODPOST_WARN=1 modules
cp qnap8528.ko /host/

echo "✅ Module erfolgreich gebaut!"
EOF
chmod +x compile.sh

# 6. Build im Container ausführen
echo "Starte Developer-Container via systemd-nspawn..."
sudo systemd-nspawn --quiet --bind="$DIR:/host" --image=flatcar_developer_container.bin /host/compile.sh

# 7. Aufräumen, um Speicherplatz freizugeben
sudo rm -f flatcar_developer_container.bin compile.sh
echo "🎉 Fertig. Module liegen in ./module/"

Das Skript liefert zwei Kernel-Module im Ordner module/: qnap8528.ko und drivetemp.ko.

Treiber-Injektion und CoolerControl per Docker Compose

Um den Flatcar-Host modifikationsfrei zu halten, übernimmt ein kleiner Alpine-Container (driver-modules-loader) beim Booten das Laden der Module in den Host-Kernel (SYS_MODULE). Erst wenn dieser Container über seinen Healthcheck meldet, dass qnap8528 geladen ist, startet der CoolerControl-Daemon.

Code:
services:
  # ---------------------------------------------------------
  # SERVICE 1: Kernel-Modul-Loader (Einmalig beim Boot)
  # ---------------------------------------------------------
  driver-modules-loader:
    container_name: driver-modules-loader
    image: alpine:latest
    restart: unless-stopped
    cap_drop:
      - ALL
    cap_add:
      - SYS_MODULE
    volumes:
      - ./module:/module:ro
      - /lib/modules:/lib/modules:ro
    healthcheck:
      test: ["CMD-SHELL", "grep -q qnap8528 /proc/modules"]
      interval: 5s
      timeout: 2s
      retries: 3
      start_period: 5s
    command:
      - /bin/sh
      - -c
      - |
        echo "Injecting qnap fan driver..."
        if ! lsmod | grep -q qnap8528; then
          insmod /module/qnap8528.ko skip_hw_check=true || { echo "Failed to load qnap8528"; exit 1; }
        fi
 
        echo "Injecting HDD temperature drivers..."
        if ! lsmod | grep -q drivetemp; then
          insmod /module/drivetemp.ko || echo "Failed to load drivetemp."
        fi
 
        echo "✅ Kernel modules ready."
        sleep infinity

  # ---------------------------------------------------------
  # SERVICE 2: CoolerControl Web-UI & Daemon
  # ---------------------------------------------------------
  coolercontrol:
    container_name: coolercontrol
    image: coolercontrol/coolercontrold:latest
    restart: unless-stopped
    depends_on:
      driver-modules-loader:
        condition: service_healthy
    cap_add:
      - SYS_RAWIO
      - SYS_ADMIN
    security_opt:
      - apparmor:unconfined
    devices:
      - /dev/sda:/dev/sda
      - /dev/sdb:/dev/sdb
      - /dev/sdc:/dev/sdc
      - /dev/sdd:/dev/sdd
    volumes:
      - /sys:/sys
      - /dev:/dev
      - /run/udev:/run/udev:ro
      - ./cc-config:/etc/coolercontrol
    networks:
      - caddy_rp_net
 
networks:
  caddy_rp_net:
    external: true

Über die CoolerControl-Weboberfläche lässt sich der 120-mm-Gehäuselüfter des QNAP TS-453D nun abhängig von Festplatten- und CPU-Temperaturen regeln.



5. CLI-Komfort ohne Paketmanager: Der Editor Micro​


Da Flatcar bewusst minimal ausgeliefert wird, fehlt im Terminal selbst ein einfacher Texteditor wie nano (lediglich vi ist vorhanden).

Um den modernen Terminal-Editor Micro bereitzustellen, ohne Binärdateien unkontrolliert ins System zu kopieren, übernimmt ein flüchtiger Alpine-Hilfscontainer die Installation nach /opt/bin (einem der wenigen beschreibbaren Pfade im Flatcar-Suchpfad).

micro/.env

Bash:
VERSION=2.0.15

micro/compose.yaml

Code:
services:
  install-micro:
    image: alpine:latest
    environment:
      - VERSION=${VERSION}
    volumes:
      - /opt/bin:/opt/bin
    command:
      - /bin/sh
      - -c
      - |
        set -e
        apk add --no-cache curl tar
        cd /tmp
 
        FILE="micro-$${VERSION}-linux64.tar.gz"
 
        echo "Lade Micro v$$VERSION herunter..."
        curl -sLO "https://github.com/micro-editor/micro/releases/download/v$${VERSION}/$$FILE"
        curl -sLO "https://github.com/micro-editor/micro/releases/download/v$${VERSION}/$${FILE}.sha"
 
        EXPECTED=$$(cat "$${FILE}.sha" | sed 's/sha256://' | cut -d' ' -f1)
        ACTUAL=$$(sha256sum "$$FILE" | cut -d' ' -f1)
 
        if [ "$$EXPECTED" != "$$ACTUAL" ] || [ -z "$$EXPECTED" ]; then
          echo "FATAL: Checksum mismatch or file not found!"
          exit 1
        fi
 
        echo "Prüfsumme ($$ACTUAL) verifiziert. Installiere nach /opt/bin..."
        tar -xzf "$$FILE" -C /opt/bin --strip-components=1 "micro-$${VERSION}/micro"
        echo "Installation abgeschlossen."

Ein einmaliges docker compose up lädt das passende Release herunter, prüft den SHA256-Hash und legt das Binary in /opt/bin/micro ab – danach steht der Editor in jeder SSH-Session bereit.



6. Der Anwendungs-Stack: Containerisierte Dienste​


Alle Anwendungsdienste sind in übersichtliche Projektordner unter /projects/ strukturiert und über ein gemeinsames Docker-Netzwerk (caddy_rp_net) verbunden.

6.1 Reverse-Proxy mit automatischer Wildcard-DNS-Challenge (Caddy)​


Caddy verwaltet alle internen Web-Oberflächen. Durch die Cloudflare-DNS-Challenge erhält das System vollwertige TLS-Zertifikate, ohne dass Portweiterleitungen am Internet-Router geöffnet werden müssen. Ein eigenes Dockerfile kompiliert Caddy mit dem nötigen DNS-Provider-Plugin.

Das Konzept und die Motivation

Der Hauptgrund für dieses Setup ist die Herstellung einer echten, validen SSL-Verbindung. Während man lokal theoretisch mit selbstsignierten Zertifikaten arbeiten könnte, führt dies oft zu Problemen: Moderne Betriebssysteme wie iOS und Android sind extrem strikt, was die Zertifikatsprüfung angeht. Besonders Hintergrundprozesse oder Apps verweigern oft den Dienst, wenn kein von einer anerkannten Zertifizierungsstelle (CA) signiertes Zertifikat vorliegt.

Um dies zu lösen, nutzen wir die DNS-01 Challenge. Anstatt dass der Zertifizierungsdienst den Server über Port 80/443 von außen prüfen muss (HTTP-01), beweist Caddy über die Cloudflare-API, dass die Domäne euch gehört, indem ein temporärer TXT-Record im DNS gesetzt wird.

Der große Vorteil: Ihr erhaltet ein "echtes" Zertifikat, das von jedem Gerät weltweit akzeptiert wird, ohne dass ihr einen einzigen Port in der Fritzbox nach außen öffnen müsst. Die Verbindung bleibt komplett intern, ist aber perfekt verschlüsselt.

Kosten für [acht Ziffern].xyz => ~ 0.80 € im Jahr. Auch wenn eine Webdomäne für rein lokale Zwecke totaler Blödsinn ist. Ist es leider die einzig richtig und so vorhergesehen Lösung für echte verschlüsselte HTTP Verbindungen. Und der Preis ist wirklich schmerzfrei.

Schritt-für-Schritt Einrichtung

1. Cloudflare DNS Konfiguration
Im Cloudflare-Dashboard wird für die Domäne ein A-Record angelegt, der auf die lokale LAN-IP des QNAP-NAS verweist (z. B. 192.168.178.20).
* Wichtig: Der "Proxy"-Status (die orangefarbene Wolke) muss auf Disabled (DNS only) stehen, da Cloudflare nicht in euer lokales Netzwerk routen kann.

2. Fritzbox: DNS-Rebinding-Schutz deaktivieren
Die Fritzbox blockiert standardmäßig DNS-Antworten, die eine lokale IP zurückgeben, um sogenannte DNS-Rebinding-Angriffe zu verhindern. Damit die Domäne 12345678.xyz im LAN aufgelöst werden kann, müsst ihr eine Ausnahme hinzufügen:
  • Gehe zu Heimnetz $\rightarrow$ Netzwerk $\rightarrow$ Netzwerkeinstellungen.
  • Suche den Bereich DNS-Rebinding-Schutz.
  • Trage dort deine Domäne ein (z. B. 12345678.xyz und *.12345678.xyz).

3. Caddy mit Cloudflare-Plugin konfigurieren


Hinweis zum API-Token: Erstellt in Cloudflare einen Token mit der Berechtigung Zone:DNS:Edit, damit Caddy die TXT-Records für die Zertifikatsprüfung selbstständig setzen und löschen kann.


caddy/Dockerfile

Code:
FROM caddy:builder-alpine AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare

FROM caddy:alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

caddy/compose.yaml

Code:
services:
  caddy:
    container_name: caddy
    build: .
    ports:
      - 80:80
      - 443:443
    environment:
      - CLOUDFLARE_API_TOKEN=abcdefg
      - TZ=Europe/Berlin
    volumes:
      - ./data:/data
      - ./config:/config
      - ./Caddyfile:/etc/caddy/Caddyfile
    restart: always
    networks:
      - rp_net

networks:
  rp_net:
    name: caddy_rp_net

caddy/Caddyfile

Code:
*.12345678.xyz {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}
    }

    @arcane host arcane.12345678.xyz
    handle @arcane {
        reverse_proxy arcane:3552
    }

    @beszel host beszel.12345678.xyz
    handle @beszel {
        reverse_proxy beszel:8090
    }

    @scrutiny host scrutiny.12345678.xyz
    handle @scrutiny {
        reverse_proxy scrutiny:8080
    }

    @coolercontrol host coolercontrol.12345678.xyz
    handle @coolercontrol {
        reverse_proxy coolercontrol:11987
    }

    @immich host immich.12345678.xyz
    handle @immich {
        reverse_proxy immich:2283
    }

    @kopia host kopia.12345678.xyz
    handle @kopia {
        reverse_proxy kopia:51515
    }
}

6.2 Docker-Verwaltung mit Arcane​


Für die zentrale Übersicht und Steuerung der Stacks wird Arcane genutzt. Es liest die Compose-Dateien direkt aus dem Dateisystem aus und ermöglicht Container-Updates über eine aufgeräumte Web-UI.

Code:
services:
  arcane:
    image: ghcr.io/getarcaneapp/manager:latest
    container_name: arcane
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/arcane/data:/app/data
      - /opt/arcane/projects:/app/data/projects
    environment:
      - APP_URL=http://localhost:3552
      - PUID=1000
      - PGID=1000
      - ENCRYPTION_KEY=123
      - JWT_SECRET=123
    cgroup: host
    restart: always
    networks:
      - caddy_rp_net
 
networks:
  caddy_rp_net:
    external: true

6.3 Gehärtetes High-Performance Samba​


Statt schwerfälliger Samba-Server mit Avahi- oder WSDD-Netzwerk-Broadcasts kommt ein schlankes smbd-only-Image zum Einsatz.

Dank network_mode: host, Beschränkung auf SMB3, aktivierter SMB-Verschlüsselung, Multi-Channel-Unterstützung und asynchronem I/O (aio_read_size=1, aio_write_size=1) lastet das Setup die 2,5-GbE-Netzwerkkarte ohne CPU-Engpässe aus.

Code:
services:
  samba:
    image: ghcr.io/servercontainers/samba:smbd-only-latest
    container_name: smb
    restart: unless-stopped
    network_mode: host
    environment:
      # Protokoll-Sicherheit
      - SAMBA_GLOBAL_CONFIG_min_SPACE_protocol=SMB3
      - SAMBA_GLOBAL_CONFIG_smb_SPACE_encrypt=required
 
      # Durchsatz-Tuning für 2.5 GbE
      - SAMBA_GLOBAL_CONFIG_server_SPACE_multi_SPACE_channel_SPACE_support=yes
      - SAMBA_GLOBAL_CONFIG_aio_SPACE_read_SPACE_size=1
      - SAMBA_GLOBAL_CONFIG_aio_SPACE_write_SPACE_size=1
 
      # Zugangsdaten & Rechte
      - ACCOUNT_sambu=abcd
      - UID_sambu=1000
 
      # Freigabedefinition
      - "SAMBA_VOLUME_CONFIG_mystorage=[media]; path=/data; valid users = sambu; guest ok = no; read only = no; browseable = yes"
 
    volumes:
      - /mnt/main_hdd_8tb/media:/data

6.4 Monitoring & Festplattenzustand: Beszel & Scrutiny​

  • Beszel: Extrem schlankes Host-Monitoring. Der Agent liest Systemmetriken und via smartctl die Festplattenzustände aus.
  • Scrutiny: Führt über SYS_RAWIO tiefgehende SMART-Analysen der vier physischen Laufwerke (/dev/sda bis /dev/sdd) durch und visualisiert Trends.

beszel/compose.yaml

Code:
services:
  beszel:
    image: henrygd/beszel:latest
    container_name: beszel_hub
    restart: unless-stopped
    volumes:
      - ./data:/beszel_data
    environment:
      - APP_URL=https://beszel.12345678.xyz
    networks:
      - caddy_rp_net

  beszel-agent:
    image: henrygd/beszel-agent:alpine
    container_name: beszel_agent
    restart: unless-stopped
    network_mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /:/extra-filesystems:ro
    environment:
      - PORT=45876
      - KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5..."

networks:
  caddy_rp_net:
    external: true

scrunity/compose.yaml

Code:
services:
  scrutiny:
    image: ghcr.io/analogj/scrutiny:latest-omnibus
    container_name: scrutiny
    restart: unless-stopped
    cap_add:
      - SYS_RAWIO
    devices:
      - /dev/sda:/dev/sda
      - /dev/sdb:/dev/sdb
      - /dev/sdc:/dev/sdc
      - /dev/sdd:/dev/sdd
    volumes:
      - /run/udev:/run/udev:ro
      - ./config:/opt/scrutiny/config
      - ./influxdb:/opt/scrutiny/influxdb
    networks:
      - caddy_rp_net

networks:
  caddy_rp_net:
    external: true

6.5 Synchronisation & Medien: Syncthing & Immich​

  • Syncthing: Synchronisiert Arbeitsordner kontinuierlich zwischen Clients und dem 8-TB-Pool.
  • Immich: Leistungsfähige, selbstgehostete Foto- und Videoverwaltung mit PostgreSQL, pgvector-Erweiterung und Redis.
syncthing/compose.yaml

Code:
services:
  syncthing:
    container_name: syncthing
    image: lscr.io/linuxserver/syncthing:latest
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Berlin
    volumes:
      - ./config:/config
      - /mnt/main_hdd_8tb/syncthing_data:/data
    ports:
      - 22000:22000/tcp
      - 22000:22000/udp
      - 21027:21027/udp
    restart: unless-stopped
    healthcheck:
      test: curl -fkLsS -m 2 127.0.0.1:8384/rest/noauth/health | grep -o --json OK || exit 1
      interval: 10s
      timeout: 2s
      retries: 3
    networks:
      - caddy_rp_net

networks:
  caddy_rp_net:
    external: true

immich/compose.yaml

Code:
name: immich

services:
  immich:
    container_name: immich
    image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
    volumes:
      - ${UPLOAD_LOCATION}:/data
      - ${THUMB_LOCATION}:/data/thumbs
      - ${PROFILE_LOCATION}:/data/profile
    env_file:
      - .env
    depends_on:
      - redis
      - database
    restart: always
    networks:
      - caddy_rp_net

  machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    restart: always
    volumes:
      - ./model-cache:/cache
    env_file:
      - .env
    networks:
      - caddy_rp_net

  redis:
    container_name: immich_redis
    image: docker.io/valkey/valkey:8-alpine
    restart: always
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping || exit 1"]
    networks:
      - caddy_rp_net

  database:
    container_name: immich_postgres
    image: ghcr.io/immich-app/image-postgresql:14-vector-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USERNAME}
      POSTGRES_DB: ${DB_DATABASE_NAME}
    volumes:
      - ./postgres:/var/lib/postgresql/data
    restart: always
    networks:
      - caddy_rp_net

networks:
  caddy_rp_net:
    external: true

6.6 Datensicherung mit Kopia​


Als Backup-Lösung sichert Kopia die Daten des 8-TB-Hauptspeichers inkrementell und dedupliziert auf die interne 3-TB-Festplatte (/mnt/backup_hdd_3tb).

Code:
services:
    kopia:
        image: kopia/kopia:latest
        container_name: Kopia
        hostname: kopia
        restart: unless-stopped
        command:
            - server
            - start
            - --insecure
            - --address=0.0.0.0:51515
            - --server-username=${WEB_USER}
            - --server-password=${WEB_PASS}
        volumes:
            - ./config:/app/config
            - ./cache:/app/cache
            - ./logs:/app/logs
            - /mnt/main_hdd_8tb:/data/main:ro
            - /mnt/backup_hdd_3tb:/data/backup
            - /mnt/usb:/mnt/usb
        networks:
            - caddy_rp_net

networks:
    caddy_rp_net:
        external: true



7. Fazit & Erfahrungen aus dem Produktivbetrieb​


Der Wechsel von QTS zu Flatcar Container Linux auf dem TS-453D eliminiert alle Schwachstellen proprietärer Fertig-NAS-Betriebssysteme.


Die größten Vorteile in der Praxis:
  • Bootet viel Schneller: Etwas mehr als eine Minute und alles ist oben. Davor waren es 5 Minuten oder sogar mehr.
  • Konfiguration ist punktgenau und reproduzierbar: Ich bin auch ein großer Freund von GUIs, weil sie implizit schon erklären was wie geht ohne eine Anleitung studieren zu müssen. Aber jetzt mit KI und wenn man es auch nur einmal einrichten möchte und dann soll es für längere Zeit so bleiben. Viel besser. QTS ist oft, zu oft GUI mößig einfach nur 💩 und überladen.
  • Schlank: QTS ist und ok weil es auch möglichts viel abdecken möchte... sehr aufgeblasen. Habe ich nie gebraucht. Jetzt Installiere ich nur genau das was ich brauche.
  • Volle Datenunabhängigkeit: Sollte das QNAP-Mainboard jemals den Geist aufgeben, nimmt man die Festplatten heraus und schließt sie per SATA oder USB an jedes x-beliebige Linux-Gerät an. Kein proprietärer QNAP-LVM-Layer, kein Herumschlagen mit QuTS hero oder fremden Software-RAIDs.
  • Backup vor Redundanz: Für ein privates Homelab bietet die Kombination aus Kopia-Snapshots auf eine getrennte interne Platte und gelegentlichen Offline-Syncs auf externe Datenträger eine deutlich höhere Datensicherheit als ein klassisches RAID, das lediglich Verfügbarkeit garantiert.
  • Keine Wartung, kein Zerfall: Das System bootet in Sekunden, belegt kaum Arbeitsspeicher und verändert seinen Host-Zustand nie. Sämtliche Applikationen sind strikt gekapselt.

Mögliche Hürden:
  • Kein Klick-Interface für Speicher-Pools: Wer Festplatten per Drag-and-Drop im Browser partitionieren möchte, wird mit Flatcar nicht glücklich.
  • Kernel-Updates: Wenn Flatcar auf einen neuen Kernel-LTS-Zweig wechselt, muss das Skript build-drivers.sh einmal angestoßen werden, um die Treiber qnap8528.ko und drivetemp.ko gegen den neuen Kernel zu bauen.

Das QNAP TS-453D erweist sich in dieser Kombination als leises, zuverlässiges und "nurDasWasIchBrauch" Homelab-Fundament.
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Schwobaseggl
Vielen Dank für diesen interessanten Bericht.

Ich bin seit über 15 Jahren QNAP-Anwender mit QTS. Ich habe über die ganzen Jahre einen minimalen Admin-Aufwand gehabt und ich mag es meine Konfiguration zentral mit dem Q-Finder zu überwachen (2xNAS + 2 x Switch von QNAP).
Ein letzter Satz: Bis heute keinerlei Datenverlust !
 
  • Gefällt mir
Reaktionen: ###Zaunpfahl###
Hi, Danke für dein Feedback 😄

Das freut mich auch für dich und auch interessant das es bei dir so gut läuft.

Mein Vorgehen setzt natürlich auch einiges an Linux Wissen Voraus. Ist jetzt zwar kein besonders hohes Niveau aber trotzdem etwas das man erst mal lernen müsste. Und der Anfang ist meist das härteste.

Ich hatte leider keine so tolle Erfahrung mit QTS. Zwar kein Datenverlust. Aber viele kleine Dinge die mich immer wieder gestört haben. Also viel zu viele Optionen und Funktionen die ich alle nicht brauche, teils in GUI voll zerstreut, Container Manager GUI ist wirklich schlecht, um Updates muss man sich kümmern, für mich total komische Formatierung (Dateisysteme) der Festplatten (ohne RAID), kein btrfs, ... usw. Ab und zu gabs mal ne Fehlermeldung die dann nach einem Neustart wieder verschwunden ist, oder das OS hat sich halb aufgehangen.

In Summe hatte ich dann einfach die Schnauze voll und wollte einfach weg.

Dann hab ich mal die nicht allzu viele Zeit die ich habe genommen und mich damit beschäftigt. Und das ist das Ergebnis. Und irgendwie hatte ich schon das Bedürfnis es mit euch zu teilen. Ich finds richtig gut und bin voll zufrieden. Vielleicht kann es irgendjemanden irgendwie bereichern, selbst wenn es jemand ohne CB Account oder doch nur ein KI Agent ist. ^^
 
Oktober 2026
Zurück
Oben