Docker auf einem Linux-Server produktiv betreiben
$
/etc
Docker · systemd · Monitoring · Linux Server
Docker auf einem Linux-Server produktiv betreiben
Log-Rotation, Ressourcen-Limits und Healthchecks für den Dauerbetrieb

Docker-Container laufen im Test meist stabil, doch im Dauerbetrieb entscheiden Log-Rotation, die richtige Storage-Driver-Wahl, saubere systemd-Integration und klare Ressourcen-Limits darüber, ob ein einzelner Container den gesamten Server lahmlegt oder stabil im Hintergrund läuft. Dieser Artikel zeigt, wie Administratoren den Docker-Daemon produktionstauglich konfigurieren, Container zuverlässig automatisch starten lassen und ihren Gesundheitszustand kontinuierlich überwachen.

16 Min. Lesezeit daemon.json · systemd · cgroups · Healthchecks Docker 24.x/25.x · systemd · Linux Server

1. Warum Docker im Dauerbetrieb andere Regeln braucht

In der lokalen Entwicklungsumgebung startet ein Container mit docker run und läuft meist genau so lange, wie er gebraucht wird. Im Dauerbetrieb auf einem Linux-Server gelten andere Regeln: Container laufen Wochen oder Monate, Logdateien wachsen unbegrenzt weiter, und ein Neustart des Servers darf nicht dazu führen, dass wichtige Dienste manuell wieder hochgefahren werden müssen. Wer Docker produktiv betreibt, muss den Daemon, die Container-Konfiguration und die Systemintegration so aufsetzen, dass der Betrieb auch ohne ständiges manuelles Eingreifen stabil bleibt.

Drei Problemfelder tauchen in der Praxis am häufigsten auf: unkontrolliertes Log-Wachstum, das die Festplatte füllt, ein einzelner Container ohne Ressourcen-Limit, der den gesamten Host durch Speicher- oder CPU-Verbrauch lahmlegt, und Container, die nach einem Reboot nicht automatisch wieder starten. Die folgenden Abschnitte behandeln systematisch, wie sich Docker-Daemon, systemd-Integration, Ressourcen-Limits und Monitoring konfigurieren lassen, damit ein Linux-Server mit vielen Containern zuverlässig im Dauerbetrieb läuft.

2. Docker-Daemon-Konfiguration: Log-Rotation und Storage-Driver

Die Standardkonfiguration des Docker-Daemons ist für schnelles Ausprobieren gedacht, nicht für den Produktionsbetrieb. Ohne explizite Log-Konfiguration verwendet Docker den Log-Treiber json-file ohne Größenbegrenzung: Jede Ausgabe eines Containers landet unbegrenzt wachsend unter /var/lib/docker/containers/<id>/<id>-json.log. Bei einem Container, der viel protokolliert, etwa ein Webserver mit Access-Log auf stdout, kann diese Datei innerhalb weniger Wochen mehrere Gigabyte erreichen und im schlimmsten Fall die gesamte Root-Partition füllen. Die Lösung liegt in der Datei /etc/docker/daemon.json, in der sich max-size und max-file für den Log-Treiber global festlegen lassen.

Genauso wichtig ist die Wahl des Storage-Drivers. Seit Docker 20.10 ist overlay2 der empfohlene Standard auf allen modernen Linux-Kerneln mit ext4 oder xfs, während der ältere devicemapper-Treiber im Loopback-Modus für Produktionsumgebungen ausdrücklich ungeeignet ist und in aktuellen Docker-Versionen nicht mehr unterstützt wird. Nach jeder Änderung an daemon.json muss der Daemon mit systemctl restart docker neu gestartet werden, was normalerweise alle laufenden Container beendet, sofern live-restore nicht aktiviert ist. Mit live-restore: true bleiben Container beim Daemon-Neustart am Laufen, was Wartungsfenster für Docker-Updates erheblich verkürzt.


{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  },
  "storage-driver": "overlay2",
  "live-restore": true,
  "default-address-pools": [
    { "base": "172.30.0.0/16", "size": 24 }
  ],
  "userland-proxy": false
}

3. systemd-Integration: Zuverlässiger Start und Neustart

Der Docker-Daemon selbst läuft als systemd-Service (docker.service) und wird mit systemctl enable docker beim Booten automatisch gestartet. Das allein reicht jedoch nicht aus, um einzelne Container nach einem Reboot wieder verfügbar zu machen. Container, die mit docker run ohne Restart-Policy gestartet wurden, bleiben nach einem Neustart des Servers dauerhaft gestoppt, weil Docker sich nur dann an den zuletzt gewünschten Zustand erinnert, wenn eine Policy wie --restart unless-stopped oder --restart always gesetzt ist. Für Einzelcontainer ist die Restart-Policy also die einfachste Form der systemd-Integration, weil der Docker-Daemon selbst beim Systemstart alle Container mit passender Policy automatisch hochfährt.

Für komplexere Setups mit mehreren zusammengehörigen Containern empfiehlt sich ein eigenes systemd-Unit-File, das docker compose up und docker compose down kapselt und explizit von docker.service abhängt. Damit lässt sich der Start eines gesamten Compose-Stacks in die reguläre Boot-Reihenfolge einordnen, inklusive After=network-online.target, damit der Stack erst startet, wenn das Netzwerk tatsächlich verfügbar ist. Das verhindert Startfehler bei Containern, die beim Hochfahren sofort eine externe Datenbank oder einen DNS-Namen erreichen müssen, aber noch keine funktionierende Netzwerkverbindung haben.


# /etc/systemd/system/docker-compose-app.service
[Unit]
Description=Application Docker Compose Stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/app
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=120

[Install]
WantedBy=multi-user.target

4. Ressourcen-Limits pro Container: CPU, Memory und PIDs

Ohne explizite Limits kann ein einzelner Container theoretisch den gesamten Arbeitsspeicher und alle CPU-Kerne des Hosts beanspruchen. Das ist besonders auf Servern mit mehreren Diensten riskant: Ein fehlerhaft konfigurierter Container mit einem Memory-Leak kann den Out-Of-Memory-Killer des Kernels auslösen, der dann nicht zwingend den fehlerhaften Prozess beendet, sondern irgendeinen Prozess auf dem System, im schlimmsten Fall die Datenbank oder den SSH-Daemon. Die Flags --memory und --memory-swap bei docker run, beziehungsweise die Felder mem_limit oder deploy.resources.limits in Compose, setzen eine harte Obergrenze, bei deren Überschreitung ausschließlich der jeweilige Container vom OOM-Killer beendet wird, nicht der gesamte Host.

Genauso wichtig ist die Begrenzung der CPU-Zeit über --cpus, das über cgroups die maximale Anzahl an CPU-Kernen anteilig zuweist, sowie --pids-limit, das die Anzahl gleichzeitiger Prozesse und Threads innerhalb eines Containers begrenzt und damit sogenannte Fork-Bomben wirkungsvoll verhindert. Aktuelle Docker-Installationen nutzen cgroups v2, prüfbar mit docker info | grep -i cgroup. Der Befehl docker stats zeigt live, wie viel CPU, Speicher und Netzwerk jeder Container tatsächlich verbraucht, und ist der erste Anlaufpunkt bei der Diagnose, welcher Container einen Server ausbremst.


#!/usr/bin/env bash
# Hard limits for a single container: memory, swap, CPU shares and PIDs
docker run -d \
  --name magento-worker \
  --memory="1g" \
  --memory-swap="1g" \
  --cpus="1.5" \
  --pids-limit=200 \
  --restart unless-stopped \
  registry.mironsoft.de/magento-worker:latest

# Live resource usage across all running containers
docker stats --no-stream --format \
  "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.PIDs}}"

# Check whether the host runs cgroups v2 (required for --pids-limit accuracy)
docker info | grep -i cgroup

5. Docker Compose in Produktion: restart-Policies und Deploy-Limits

In Docker Compose lassen sich Ressourcen-Limits und Restart-Verhalten deklarativ im YAML festhalten, statt sie bei jedem docker run erneut als Flags mitzugeben. Der Schlüssel deploy.resources.limits definiert die Obergrenze für CPU und Speicher, deploy.resources.reservations legt fest, welche Ressourcen mindestens garantiert sind, was insbesondere auf Servern mit knappem Speicher für vorhersehbares Verhalten sorgt. Die Restart-Policy unless-stopped ist für die meisten produktiven Dienste die richtige Wahl, weil sie einen Container nach jedem Absturz und nach jedem Server-Reboot automatisch neu startet, ihn aber nicht erneut hochfährt, wenn er ausdrücklich mit docker stop beendet wurde.

Der healthcheck-Block in Compose ergänzt die reine Prozessüberwachung um eine anwendungsspezifische Prüfung: Ein Container kann laufen, ohne dass der dahinterliegende Dienst tatsächlich Anfragen beantwortet, etwa während einer langen Datenbankmigration beim Start. Mit test, interval, timeout und retries lässt sich definieren, wann ein Container als healthy gilt, und abhängige Services können über depends_on: condition: service_healthy erst starten, wenn diese Prüfung erfolgreich war. Das verhindert Race-Conditions, bei denen eine Anwendung startet, bevor die Datenbank tatsächlich Verbindungen akzeptiert.


# docker-compose.yml: production-ready service definition
services:
  app:
    image: registry.mironsoft.de/magento-app:2.4.8
    restart: unless-stopped
    deploy:
      resources:
        limits:
          cpus: "2.0"
          memory: 2g
        reservations:
          cpus: "0.5"
          memory: 512m
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 40s
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app-net

  db:
    image: mysql:8.0
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - app-net

volumes:
  db-data:

networks:
  app-net:
    driver: bridge

6. Monitoring und Health-Checks: Containerzustand überwachen

Die HEALTHCHECK-Anweisung im Dockerfile oder im Compose-File definiert, welcher Befehl innerhalb des Containers regelmäßig ausgeführt wird, um den Anwendungszustand zu prüfen, üblicherweise ein einfacher HTTP-Request gegen einen /health-Endpunkt oder ein Datenbank-Ping. Der Status landet direkt im Docker-Objekt und ist mit docker inspect --format='{{.State.Health.Status}}' <container> abrufbar, mit den möglichen Werten starting, healthy und unhealthy. Diese eingebaute Prüfung ist die Grundlage für automatisiertes Monitoring, weil externe Tools nicht selbst wissen müssen, wie ein einzelner Dienst geprüft wird.

Für den produktiven Betrieb reicht ein einzelner Healthcheck aber nicht aus: Ein Container, der als unhealthy markiert wird, startet ohne zusätzliche Automatisierung nicht automatisch neu. Das Zusatztool autoheal beobachtet über den Docker-Socket alle Container mit aktivem Healthcheck und startet ungesunde Container automatisch neu. Für serverweites Monitoring über mehrere Hosts hinweg exportiert cAdvisor detaillierte Container-Metriken im Prometheus-Format, während node_exporter die Host-Ebene abdeckt. Beide zusammen liefern in Grafana ein vollständiges Bild aus Container-Gesundheit, Ressourcenverbrauch und Host-Auslastung.


#!/usr/bin/env bash
# Poll all running containers and alert on unhealthy status
set -euo pipefail

for cid in $(docker ps -q); do
  name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's#^/##')
  status=$(docker inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid")

  if [[ "$status" == "unhealthy" ]]; then
    echo "[ALERT] Container $name is unhealthy, restarting" >&2
    docker restart "$cid"
    curl -s -X POST "$SLACK_WEBHOOK_URL" \
      -H 'Content-Type: application/json' \
      -d "{\"text\":\"Container $name was unhealthy and got restarted\"}"
  fi
done

7. Netzwerk- und Volume-Strategien für Persistenz und Isolation

Für persistente Daten sind benannte Volumes (docker volume create beziehungsweise das volumes-Feld in Compose) Bind Mounts in den meisten Fällen vorzuziehen, weil sie von Docker selbst verwaltet werden, plattformunabhängig funktionieren und sich nicht versehentlich mit Host-Berechtigungsproblemen vermischen. Bind Mounts bleiben sinnvoll, wenn Entwickler Quellcode während der Entwicklung live in den Container spiegeln wollen, gehören im Produktivbetrieb aber vor allem dorthin, wo Konfigurationsdateien read-only eingebunden werden. Ein Volume lässt sich einfach über einen temporären Hilfscontainer sichern, der das Volume mountet und den Inhalt per tar in ein Archiv auf dem Host schreibt.

Netzwerkseitig sollte jeder Compose-Stack ein eigenes, benutzerdefiniertes Bridge-Netzwerk erhalten, statt sich das Standardnetzwerk mit allen anderen Containern auf dem Host zu teilen. Benutzerdefinierte Netzwerke bringen automatische DNS-Auflösung zwischen Containern über den Servicenamen mit, sodass ein Anwendungscontainer die Datenbank einfach über db:3306 statt über eine feste IP-Adresse erreicht. Der Modus --network host umgeht die Netzwerkisolation vollständig und sollte nur für sehr spezifische Fälle wie Monitoring-Agenten mit Zugriff auf alle Host-Ports verwendet werden, niemals als Standard für Anwendungscontainer.

8. Sicherheit: Non-Root-Container, Capabilities und Secrets

Container laufen standardmäßig als Root-Benutzer innerhalb ihres Namespaces, was bei einem Ausbruch aus dem Container-Isolationsmechanismus, etwa durch eine Kernel-Schwachstelle, direkten Root-Zugriff auf den Host bedeuten kann. Die Anweisung USER im Dockerfile mit einer nicht-privilegierten UID reduziert dieses Risiko erheblich, ebenso wie das Flag --read-only, das das Root-Dateisystem des Containers unveränderlich macht und nur explizit deklarierte Volumes beschreibbar lässt. Für Anwendungen, die temporäre Dateien schreiben müssen, kombiniert man --read-only mit --tmpfs /tmp, um weiterhin einen beschreibbaren, aber flüchtigen Bereich bereitzustellen.

Die Linux-Capabilities eines Containers lassen sich mit --cap-drop ALL vollständig entfernen und anschließend gezielt mit --cap-add nur für tatsächlich benötigte Funktionen wieder hinzufügen, etwa NET_BIND_SERVICE für Prozesse, die auf Port 80 binden müssen. Das Flag --privileged hebt sämtliche Isolation auf und sollte in Produktionsumgebungen praktisch nie verwendet werden. Für sensible Zugangsdaten wie Datenbankpasswörter sind Docker Secrets oder gemountete Dateien Umgebungsvariablen vorzuziehen, weil Umgebungsvariablen über docker inspect und in Prozesslisten für alle Nutzer mit Docker-Zugriff sichtbar sind, während Secrets nur innerhalb des jeweiligen Containers gemountet werden.

9. Docker-Konfigurationen im direkten Vergleich

Viele Docker-Standardeinstellungen sind für lokale Entwicklung gedacht und im Produktivbetrieb ein Risiko. Die folgende Übersicht zeigt die wichtigsten Unterschiede zwischen einer unsicheren Standardkonfiguration und der empfohlenen produktionstauglichen Einstellung.

Bereich Unsicherer Standard Empfohlene Produktionseinstellung Vorteil
Log-Treiber json-file ohne Limit max-size + max-file in daemon.json Verhindert volle Root-Partition
Storage-Driver devicemapper (loopback) overlay2 Stabiler, offiziell unterstützt
Restart-Verhalten kein --restart gesetzt --restart unless-stopped Übersteht Reboot und Crash
Ressourcen keine Limits gesetzt --memory, --cpus, --pids-limit Ein Container starvt nicht den Host
Benutzer im Container root (Default) USER mit UID + --cap-drop ALL Reduziert Schaden bei Container-Escape

In der Praxis wirken sich vor allem die ersten beiden Zeilen der Tabelle am stärksten auf die Stabilität aus, weil unbegrenztes Logwachstum und ein fehlendes Ressourcen-Limit die häufigsten Ursachen für einen kompletten Server-Ausfall sind. Wer diese Defaults einmal projektweit korrigiert, etwa zentral in der daemon.json und in einer gemeinsamen Compose-Basis-Datei, muss sie nicht für jeden neuen Container erneut einzeln setzen.

Mironsoft

Docker-Infrastruktur, systemd-Automatisierung und Server-Hardening für produktive Linux-Umgebungen

Docker-Infrastruktur, die auch nachts durchläuft?

Wir analysieren bestehende Docker-Setups, identifizieren fehlende Ressourcen-Limits und unsichere Defaults und richten Daemon-Konfiguration, systemd-Integration und Monitoring so ein, dass eure Container-Infrastruktur zuverlässig im Dauerbetrieb läuft.

Daemon-Audit

Log-Rotation, Storage-Driver und Ressourcen-Limits produktionsreif konfigurieren

systemd-Integration

Zuverlässiger Start, geordnete Boot-Reihenfolge und automatischer Neustart nach Ausfällen

Monitoring-Setup

Healthchecks, cAdvisor und Alerting für Container-Gesundheit einrichten

10. Zusammenfassung

Docker auf einem Linux-Server produktiv betreiben heißt vor allem, die komfortablen Standardeinstellungen für lokale Entwicklung durch explizite, produktionstaugliche Konfiguration zu ersetzen. Log-Rotation über max-size und max-file in der daemon.json verhindert eine volle Root-Partition. Der Storage-Driver overlay2 ist die stabile, offiziell unterstützte Wahl. systemd-Integration über Restart-Policies und eigene Unit-Files sorgt dafür, dass Container und ganze Compose-Stacks nach jedem Reboot zuverlässig wieder hochfahren. Ressourcen-Limits über --memory, --cpus und --pids-limit stellen sicher, dass ein fehlerhafter Container nicht den gesamten Host mit in den Absturz reißt.

Der letzte, oft unterschätzte Baustein ist kontinuierliches Monitoring: Healthchecks in Dockerfile oder Compose-File, kombiniert mit automatischem Neustart über Tools wie autoheal und serverweiter Metrik-Erfassung über cAdvisor und node_exporter, verwandeln reaktives Feuerlöschen in proaktive Fehlererkennung. Wer diese fünf Bereiche einmal sauber aufsetzt und in eine gemeinsame daemon.json sowie eine Compose-Basis-Konfiguration überführt, muss sie nicht für jeden neuen Container erneut einzeln durchdenken.

Docker auf einem Linux-Server produktiv betreiben: Das Wichtigste auf einen Blick

Log-Rotation

max-size/max-file in /etc/docker/daemon.json setzen, sonst füllt json-file die Root-Partition.

Ressourcen-Limits

--memory, --cpus, --pids-limit pro Container, damit einer nicht den ganzen Host starvt.

systemd-Integration

Restart-Policy unless-stopped oder eigenes Unit-File mit After=network-online.target.

Monitoring & Healthchecks

HEALTHCHECK, autoheal und cAdvisor/node_exporter für automatische Erkennung und Reaktion.

11. FAQ: Docker auf einem Linux-Server produktiv betreiben

1Warum wächst die Docker-Log-Datei unbegrenzt und wie verhindere ich das?
Ohne Konfiguration nutzt Docker json-file ohne Limit. max-size und max-file in daemon.json global setzen, danach Docker neu starten.
2Welcher Storage-Driver ist für Produktion empfohlen?
overlay2 auf modernen Kerneln mit ext4 oder xfs. devicemapper im Loopback-Modus ist ungeeignet und in aktuellen Versionen nicht mehr unterstützt.
3Was bewirkt live-restore in der daemon.json?
Container laufen beim Daemon-Neustart weiter, statt beendet zu werden. Verkürzt Wartungsfenster bei Docker-Updates erheblich.
4Wie starte ich Container nach einem Server-Reboot automatisch?
--restart unless-stopped für Einzelcontainer. Für Compose-Stacks zusätzlich ein eigenes systemd-Unit-File mit After=network-online.target.
5Welche Restart-Policy ist für produktive Dienste richtig?
unless-stopped: startet nach Absturz und Reboot automatisch neu, bleibt aber gestoppt nach explizitem docker stop.
6Wie verhindere ich, dass ein Container den gesamten Server-Speicher verbraucht?
--memory und --memory-swap setzen, beziehungsweise deploy.resources.limits in Compose. OOM-Killer beendet dann nur den einzelnen Container.
7Was ist der Unterschied zwischen HEALTHCHECK und Restart-Policy?
Restart-Policy reagiert auf beendete Prozesse. HEALTHCHECK prüft, ob die Anwendung im laufenden Container tatsächlich funktioniert. autoheal koppelt beide Mechanismen.
8Wann sollte ich named Volumes statt Bind Mounts nutzen?
Für persistente Produktionsdaten immer named Volumes. Bind Mounts eignen sich für Entwicklung oder read-only Konfigurationsdateien.
9Ist --privileged jemals in Produktion sinnvoll?
Praktisch nie. Gezielte Capabilities über --cap-add nach --cap-drop ALL decken fast alle legitimen Fälle sicherer ab.
10Wie überwache ich mehrere Docker-Hosts zentral?
cAdvisor für Container-Metriken, node_exporter für die Host-Ebene, beide zusammen in Grafana als zentrales Dashboard.