Media Server Dashboard

Statisches Homelab-Dashboard mit Live-Speicheranzeige für einen gemounteten Pfad (/downloads). Die Weboberfläche listet Media-Server-Dienste als Kacheln und zeigt den aktuellen Speicherplatz des Download-Laufwerks an.

Übersicht

Komponente Aufgabe
nginx Liefert die statische Website (index.html) und die Datei disk.json aus
diskinfo Liest per df den Speicher des gemounteten Pfads und schreibt alle 30 Sekunden disk.json
Browser Lädt disk.json per Javalescript und aktualisiert die Speicheranzeige
┌─────────────┐     GET /disk.json      ┌─────────────┐
│   Browser   │ ◄────────────────────── │    nginx    │
└─────────────┘                         │  (Port 80)  │
                                        └──────▲──────┘
                                               │ liest
                                        /opt/nginx/html/disk.json
                                               ▲
                                               │ schreibt alle 30s
                                        ┌──────┴──────┐
                                        │   diskinfo  │
                                        │   (alpine)  │
                                        └──────┬──────┘
                                               │ df -h
                                        /mnt/ssd → /downloads

Projektstruktur

.
├── docker-compose.yml       # Container-Definition (nginx + diskinfo)
├── html/
│   ├── index.html           # Dashboard (Apps + Speicheranzeige)
│   ├── disk.json            # Wird vom diskinfo-Container geschrieben
│   ├── filebot-media-browser.svg
│   ├── redirect.html
│   └── bk_index.htm         # Backup der alten index.html
└── scripts/
    └── update-disk.sh       # Speicher auslesen + Endlosschleife

Voraussetzungen

  • Docker und Docker Compose auf dem Host
  • Gemounteter Speicherpfad auf dem Host (Standard: /mnt/ssd)
  • Port 80 frei

Deployment

1. Dateien auf den Server kopieren

Auf dem Server liegt das Projekt typischerweise unter /opt/nginx:

/opt/nginx/
├── docker-compose.yml
├── html/
└── scripts/
    └── update-disk.sh

Dateien aus diesem Repository dorthin kopieren, z. B. per git clone, rsync oder scp.

Alternativ kann update-disk.sh direkt auf dem Server angelegt werden:

cat > /opt/nginx/scripts/update-disk.sh << 'EOF'
#!/bin/sh

MOUNT="/downloads"
OUTPUT="/html/disk.json"

update_disk() {
    LINE=$(df -h "$MOUNT" 2>/dev/null | awk 'NR==2')
    if [ -z "$LINE" ]; then
        echo "ERROR: df failed for $MOUNT" >&2
        return 1
    fi

    TOTAL=$(echo "$LINE" | awk '{print $2}')
    USED=$(echo "$LINE" | awk '{print $3}')
    AVAILABLE=$(echo "$LINE" | awk '{print $4}')
    USE_PERCENT=$(echo "$LINE" | awk '{print $5}')

    cat > "$OUTPUT" <<JSON
{
  "total": "$TOTAL",
  "used": "$USED",
  "available": "$AVAILABLE",
  "use_percent": "$USE_PERCENT",
  "updated_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
JSON

    echo "disk.json updated: $USE_PERCENT used ($USED / $TOTAL)"
}

while true; do
    update_disk || true
    sleep 30
done
EOF

Vorher sicherstellen, dass das Verzeichnis existiert:

mkdir -p /opt/nginx/scripts

2. Pfade in docker-compose.yml anpassen

Im Repository sind relative Pfade (./html, ./scripts) gesetzt. Auf dem Server können absolute Pfade verwendet werden:

services:
  nginx:
    volumes:
      - /opt/nginx/html:/usr/share/nginx/html:ro
      - /mnt/ssd:/downloads:ro

  diskinfo:
    volumes:
      - /mnt/ssd:/downloads:ro
      - /opt/nginx/html:/html
      - /opt/nginx/scripts/update-disk.sh:/update-disk.sh:ro

Wichtig: Der Host-Pfad /mnt/ssd muss auf das Laufwerk zeigen, dessen Speicher angezeigt werden soll. Beide Container mounten ihn als /downloads (nur lesend).

3. Container starten

cd /opt/nginx
docker compose up -d

4. Funktion prüfen

# Container-Status
docker compose ps

# diskinfo-Logs (sollte alle ~30s eine Zeile ausgeben)
docker compose logs -f diskinfo

# Generierte JSON-Datei
cat /opt/nginx/html/disk.json

Erwartete Log-Ausgabe:

diskinfo  | disk.json updated: 48% used (213.7G / 468.2G)

Erwarteter Inhalt von disk.json:

{
  "total": "468.2G",
  "used": "213.7G",
  "available": "254.5G",
  "use_percent": "48%",
  "updated_at": "2026-06-05T12:08:16Z"
}

Das Feld updated_at dient zur Kontrolle, ob die Datei regelmäßig aktualisiert wird.

Wie die Speicheranzeige funktioniert

Backend (scripts/update-disk.sh)

  1. df -h /downloads liefert Belegung des gemounteten Pfads
  2. Werte werden als JSON nach /html/disk.json geschrieben
  3. Eine while true-Schleife wiederholt das alle 30 Sekunden

Die Schleife liegt bewusst im Shell-Skript und nicht in der docker-compose.yml. So werden YAML-Parsing-Probleme (z. B. in Portainer) vermieden.

Frontend (html/index.html)

  • Beim Laden der Seite: fetch('/disk.json')
  • Alle 30 Sekunden: erneuter Abruf per setInterval
  • Anzeige: Prozent, Belegt, Frei, Gesamt sowie Fortschrittsbalken

Docker Compose Referenz

services:
  nginx:
    image: nginx:latest
    container_name: nginx
    restart: unless-stopped
    ports:
      - 80:80
    volumes:
      - ./html:/usr/share/nginx/html:ro
      - /mnt/ssd:/downloads:ro

  diskinfo:
    image: alpine:latest
    container_name: diskinfo
    restart: unless-stopped
    volumes:
      - /mnt/ssd:/downloads:ro
      - ./html:/html
      - ./scripts/update-disk.sh:/update-disk.sh:ro
    command: ["/bin/sh", "/update-disk.sh"]

Hinweise:

  • Beim diskinfo-Service kein entrypoint setzen
  • command als Array: ["/bin/sh", "/update-disk.sh"]
  • Die while-Schleife nicht in command der Compose-Datei definieren

Was nicht funktioniert

Diese Variante führt zu Syntaxfehlern, weil YAML/Docker den Befehl an Semikolons zerlegt:

# ❌ Nicht verwenden
entrypoint: ["/bin/sh", "-c"]
command: while true; do sh /update-disk.sh; sleep 30; done

Fehler im Log:

diskinfo | true: line 0: syntax error: unexpected end of file (expecting "do")

Apps anpassen

Die Dienste-Kacheln werden in html/index.html im apps-Array konfiguriert:

const apps = [
    { name: "Dockage", port: 5001, icon: "...", color: "#3498db" },
    { name: "SabNZBd", port: 8080, icon: "...", color: "#3498db" },
    // ...
];
Feld Beschreibung
name Anzeigename
port Port auf dem Host (IP = window.location.hostname)
icon URL oder Pfad zum Icon
color Kachel-Hintergrundfarbe
protocol Optional: http (Standard) oder https
vnc Optional: VNC-Badge und Hinweis-Toast
extra Optional: URL-Suffix (z. B. für Krusader VNC)

Nach Änderungen an index.html reicht ein Browser-Reload; kein Container-Neustart nötig.

Fehlerbehebung

diskinfo exited with code 0 (ständig neu startend)

Ursache: Alte Version von update-disk.sh ohne while true-Schleife. Das Skript läuft einmal durch und beendet sich.

Prüfen:

tail -5 /opt/nginx/scripts/update-disk.sh

Am Ende muss stehen:

while true; do
    update_disk || true
    sleep 30
done

Lösung: Aktuelles Skript aus diesem Repository nach /opt/nginx/scripts/update-disk.sh kopieren (siehe Deployment) oder per cat > ... << 'EOF' direkt auf dem Server anlegen, dann Container neu erstellen:

docker compose up -d --force-recreate diskinfo

disk.json bleibt bei 93 Bytes / alte Werte

  • Altes JSON-Format ohne updated_at → Skript auf dem Server ist veraltet
  • Neues Format ist ca. 130+ Bytes groß

Speicheranzeige zeigt „Fehler“

# Mount im Container prüfen
docker compose exec diskinfo df -h /downloads

# Schreibrechte prüfen
docker compose exec diskinfo sh -c 'touch /html/test && rm /html/test && echo OK'

diskinfo zeigt ERROR: df failed for /downloads

  • Host-Pfad /mnt/ssd existiert nicht oder ist nicht gemountet
  • Volume-Mapping in docker-compose.yml prüfen

Nginx liefert disk.json, Werte ändern sich aber nicht

  1. docker compose ps diskinfo → Status muss Up sein (nicht Restarting)
  2. docker compose logs diskinfo → regelmäßige disk.json updated-Zeilen
  3. watch -n 5 cat /opt/nginx/html/disk.jsonupdated_at muss sich ändern

Wartung

# Container neu starten
docker compose restart

# Nur diskinfo neu erstellen (nach Skript-Update)
docker compose up -d --force-recreate diskinfo

# Logs anzeigen
docker compose logs -f

# Container stoppen
docker compose down

Bekannte Einschränkungen

  • Die Speicheranzeige bezieht sich auf das Filesystem des Mounts, nicht auf den Inhalt eines Unterordners
  • nginx mountet /downloads nur lesend; Schreibzugriff erfolgt ausschließlich über diskinfo auf /html
  • Service-Status (grüner Punkt) nutzt fetch mit no-cors und kann je nach Browser/Dienst ungenau sein
  • Apple Touch Icons (apple-touch-icon.png etc.) sind nicht vorhanden → 404 in den nginx-Logs (harmlos)
S
Description
No description provided
Readme
40 KiB
Languages
HTML 97.6%
Shell 2.4%