devmaker.net
start/ homelab/ no-space-left-on-device-docker-logs-killten-post
Homelab

No space left on device: Docker-Logs killten Postgres

„HTTP 500" auf der ganzen Seite, und im Log nur `could not translate host name "db"` – das klingt nach kaputtem DNS, war aber eine volle Festplatte. Auf meinem Homelab-Server hatten nie rotierte Docker-Logs `/var` bis auf 0 Byte gefüllt, woraufhin der geteilte Postgres-Container in eine endlose Crashloop ging und gleich mehrere Django-Sites lahmlegte. Dieser Artikel zeigt an meinem echten Vorfall, wie du die Ursache in Minuten findest, den Service sofort wiederherstellst und – wichtiger – mit sauberer Log-Rotation dafür sorgst, dass es nicht wiederkommt. Du brauchst Docker-Grundlagen, aber keine tiefen DBA-Skills.

Harry_im_Homelab31 (Portrait)
Harald
2026-07-13 · ~11 Min. Lesezeit
Volle Platte, gecrashte DB: Wenn nie rotierte Docker-Logs den Server lahmlegen.
Volle Platte, gecrashte DB: Wenn nie rotierte Docker-Logs den Server lahmlegen.

Das Symptom: 500 überall, ein DNS-Fehler im Log

Ein ganz normaler Tag, dann die Meldung: Die Seite ist offline. Der schnelle Check zeigt keinen Timeout und keinen 502, sondern einen sauberen HTTP 500. Der Webserver läuft also, aber jeder Request stirbt in der Anwendung. Und im Django-Log steht immer dieselbe Zeile:

bash
$ curl -sS -o /dev/null -w "HTTP %{http_code}\n" https://example.net/
HTTP 500

# Django-Log:
OperationalError: could not translate host name "db" to address:
Name or service not known

could not translate host name "db" schreit förmlich „DNS kaputt“ oder „Netzwerk weg“. Genau das war die falsche Fährte, die mich kurz in die Irre führte.

Die falsche Fährte – und die echte Ursache

In meinem Setup ist db kein DNS-Name, sondern der Netzwerk-Alias eines geteilten Postgres-Containers, den sich mehrere Sites teilen. Ist dieser Container nicht da, existiert der Alias nicht – und der Fehler sieht aus wie ein DNS-Problem, obwohl die Datenbank schlicht fehlt. Also nicht am Netzwerk basteln, sondern zuerst die Container ansehen:

bash
$ docker ps -a --format '{{.Names}}\t{{.Status}}'
...
postgresql-db-1    Restarting (1) 27 seconds ago

$ docker logs postgresql-db-1 --tail 5
PostgreSQL Database directory appears to contain a database; Skipping initialization
FATAL:  could not write lock file "postmaster.pid": No space left on device

Da ist sie: No space left on device. Postgres kann nicht einmal seine postmaster.pid schreiben, crasht sofort, wird von Docker neu gestartet, crasht wieder – eine Endlosschleife mit vierstelligem Restart-Counter. Die Ursache liegt also nicht in der Datenbank, sondern eine Ebene tiefer: die Platte ist voll.

Diagnose: Wer frisst die Platte?

Erst bestätigen, dass wirklich kein Byte frei ist, dann den Verursacher unter /var/lib/docker einkreisen:

bash
$ df -h /var
/dev/vda4  305G  290G  0  100% /var        # 0 Byte frei

$ du -sh /var/lib/docker/* | sort -rh | head -4
159G  /var/lib/docker/overlay2
 83G  /var/lib/docker/containers            # <- verdächtig groß
 27G  /var/lib/docker/volumes

# Die größten Container-Logs den Namen zuordnen:
$ for id in $(docker ps -aq); do
    f=$(docker inspect --format '{{.LogPath}}' "$id")
    [ -f "$f" ] && printf '%s\t%s\n' \
      "$(du -m "$f"|cut -f1)MB" "$(docker inspect --format '{{.Name}}' "$id")"
  done | sort -rh | head
70981MB  /gitlab_ce-web-1
10389MB  /app-worker-1

Der Fund: ein einzelnes *-json.log von GitLab mit 70 GB, dazu ein Celery-Worker mit 11 GB. Zusammen fast der gesamte belegte Platz im containers-Verzeichnis. Der Grund ist unspektakulär: Docker rotiert die stdout-Logs des json-file-Treibers standardmäßig nicht. Ein geschwätziger Container läuft damit über Wochen ungebremst voll – bis eben nichts mehr geht.

Sofort-Fix: Service in zwei Minuten zurück

Wichtig: die Logdatei truncaten, nicht löschen. Ein laufender Container hält den Filehandle offen – rm gäbe den Platz erst beim Neustart frei, : > datei leert sofort und der Container schreibt einfach weiter.

Nur stdout-Logs anfassen. Datenbank-Volumes und Nutzdaten bleiben tabu – die stecken in /var/lib/docker/volumes, nicht in den json-Logs.

bash
# a) die größten Logs leeren (Pfade aus der Diagnose)
: > /var/lib/docker/containers/<id>/<id>-json.log
df -h /var        # Platz ist sofort wieder frei

# b) die gecrashte DB neu starten
docker restart postgresql-db-1
docker exec postgresql-db-1 pg_isready   # -> accepting connections

# c) App-Stack neu starten und prüfen
docker restart app worker beat
curl -sS -o /dev/null -w "HTTP %{http_code}\n" https://example.net/   # -> 200

Zwei geleerte Logdateien, ein DB-Neustart, ein App-Neustart – und die Seite liefert wieder 200. Das war der Feuerwehr-Teil. Der eigentlich interessante Teil kommt jetzt: dafür sorgen, dass es nicht wiederkommt.

Der Denkfehler bei der Log-Rotation

„Kein Problem, ich hab doch Log-Rotation in der daemon.json“ – dachte ich. Genau da lag der Denkfehler. Die Datei war gesetzt, aber der 70-GB-Ausreißer entstand trotzdem. Der Grund: der laufende Docker-Daemon hatte die Config nie geladen.

bash
$ stat -c '%y' /etc/docker/daemon.json
2026-03-25 15:06:...          # Config zuletzt geändert

$ systemctl show docker -p ActiveEnterTimestamp
ActiveEnterTimestamp=2025-12-20 17:21:...   # Daemon läuft seit VOR der Änderung

Die daemon.json ist neuer als der laufende Daemon – also nie eingelesen. Und Achtung: ein systemctl reload docker (SIGHUP) lädt die Default-Log-Optionen nicht nach; nur ein voller systemctl restart docker zieht sie – und der startet alle Container neu. Auf einer Box mit mehreren produktiven Stacks ist das nichts, was man mal eben nebenbei macht.

Der dauerhafte Fix: Rotation pro Stack im Compose

Statt mich auf einen unsichtbaren Daemon-Default zu verlassen, verankere ich die Rotation explizit im Compose jedes Stacks. Vorteile: sie ist im docker inspect sichtbar, überlebt jedes Recreate und braucht keinen globalen Docker-Restart. Ein YAML-Anchor hält es DRY:

yaml
# oben im docker-compose.yml
x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  web:
    logging: *default-logging
    # ...
  worker:
    logging: *default-logging
    # ...
bash
cp docker-compose.yml docker-compose.yml.bak.$(date +%Y%m%d)   # Backup zuerst
docker compose config --quiet && echo OK                       # YAML validieren
docker compose up -d                                           # recreate

# Verifizieren – muss die Opts zeigen, nicht map[]:
docker inspect web --format '{{.HostConfig.LogConfig.Config}}'
# map[max-file:3 max-size:10m]

Der letzte Schritt ist der wichtigste: erst wenn inspect map[max-file:3 max-size:10m] statt map[] zeigt, ist die Rotation wirklich aktiv. Ein leeres map[] heißt „hängt am Daemon-Default“ – und der war ja gerade das Problem.

Prävention: ein simpler Disk-Watch

Rotation verhindert das Wiederkehren dieses konkreten Falls. Aber die nächste Platte läuft aus einem anderen Grund voll (ein Volume, ein Backup, ein Image-Wildwuchs). Deshalb noch ein billiger Wächter per Cron: warnt ab 85 % und leert ab 88 % automatisch durchgelaufene Logs – Selbstheilung für genau diesen Ausfalltyp.

bash
#!/usr/bin/env bash
# /usr/local/bin/disk-watch.sh
set -uo pipefail
LOG=/var/log/disk-watch.log; WARN=85; HEAL=88; LOGMAX_MB=500
CDIR=/var/lib/docker/containers
log(){ echo "$(date '+%F %T') $*" >> "$LOG"; }
for mp in /var /; do
  use=$(df --output=pcent "$mp" | tail -1 | tr -dc '0-9')
  [ "$use" -lt "$WARN" ] && continue
  log "WARN $mp bei ${use}%"
  if [ "$use" -ge "$HEAL" ]; then
    find "$CDIR" -name '*-json.log' -size +${LOGMAX_MB}M \
      | while read -r f; do : > "$f"; log "  HEAL geleert: $f"; done
  fi
done
bash
# /etc/cron.d/disk-watch
*/15 * * * * root /usr/local/bin/disk-watch.sh

Was ich weggelassen habe

  • Kein Push-Alarm (noch nicht). Der Watch loggt nur lokal. Ein Push aufs Handy wäre besser, aber ich wollte dafür keinen fremden Cloud-Dienst – die datenschutzfreundliche Variante (self-hosted ntfy oder eine Nextcloud-Notification) kommt in einem eigenen Schritt.
  • Den geteilten DB-Container nicht neu erstellt. Für saubere Rotation müsste auch postgresql-db-1 einmal recreated werden – das hätte aber alle Sites kurz offline genommen. Bei 194 MB Log-Größe war das Risiko den erzwungenen Cross-Site-Ausfall nicht wert; das mache ich im nächsten Wartungsfenster.
  • Kein Prometheus/Grafana. Für einen einzelnen Homelab-Server ist ein 20-Zeilen-Cron mehr wert als ein Monitoring-Stack, den ich selbst wieder pflegen und dessen Volumes volllaufen könnten. Bewusst pragmatisch, nicht überengineered.

Fazit

Die eigentliche Ursache war langweilig – eine volle Platte. Interessant waren die zwei Fallen drumherum: der irreführende DNS-Fehler, hinter dem eine gecrashte DB steckte, und die scheinbar vorhandene, aber inaktive Log-Rotation in der daemon.json. Zwei Lehren bleiben: Rotation gehört dorthin, wo man sie sieht und prüfen kann – ins Compose, verifiziert per inspect. Und ein dummer, selbstheilender Disk-Watch schlägt ein hübsches Dashboard, das im Ernstfall selbst mit ausfällt. Wer seine geteilte Datenbank wirklich absichern will, kombiniert das mit echten Backups – dazu mehr im Artikel über Postgres-Backups für Self-Hosted Apps.

// verwandte beiträge

> echo "your thoughts" >> no-space-left-on-device-docker-logs-killten-post.responses

Schreibe einen Kommentar

Wird für die Bestätigung benötigt