Weg A oder Weg B? Systemd oder Docker: Welches Laufzeitmodell für deine Dienste?

Cyber-Panda als Psychologe zwischen zwei Wegen Bild: Der Panda analysiert deine Laufzeitentscheidung.

Einleitung

Du betreibst einen Dienst auf einem Linux-Server. Vielleicht eine kleine API, ein Worker für Hintergrundjobs oder ein Cron, der alle fünf Minuten etwas pusht. Die Frage ist: Packst du ihn in einen Docker-Container oder definierst du eine systemd-Unit? Beide Wege führen zum Ziel, aber sie unterscheiden sich in Wartung, Overhead und Mental Model. Ich habe beide Varianten in Produktion verwendet — hier ist meine Entscheidungslogik.

Kontext

Wir reden hier über einen einzelnen Host, kein Cluster, kein Kubernetes. Die Frage ist also nicht “Container-Orchestrierung ja oder nein”, sondern “welches Laufzeitmodell ist für diesen Dienst auf dieser Maschine richtig”.

Wichtige Constraints für mich:

  • Ressourcen: Der Server hat 4 GB RAM, kein Overhead-Budget für eine Virtualisierung.
  • Rolle: Ich bin alleiniger Maintainer des Dienstes.
  • Anforderung: Restarts bei Crash, Logging-Zugriff, einfaches Update-Verfahren.

Non-Goals: Multi-Host-Skalierung, Immutable Infrastructure auf Cloud-Ebene, Image-Build-Pipelines mit Registry.

Option A: Systemd-Unit

Die “traditionelle” Linux-Variante. Du schreibst eine .service-Datei, verlinkst sie nach /etc/systemd/system/ und lässt systemd den Prozess starten, überwachen und bei Bedarf neustarten.

Beispiel Unit:

# /etc/systemd/system/mein-dienst.service
[Unit]
Description=Mein Hintergrunddienst
After=network.target

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/mein-dienst
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=/opt/mein-dienst/.env

StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Pros:

  • Zero Overhead: Kein Daemon, kein Shim, kein zusätzlicher Prozess außer systemd selbst.
  • Direkter Log-Zugriff über journalctl -u mein-dienst.
  • Native Systemintegration: systemctl status, systemctl restart, systemctl enable.
  • Keine Image-Registry nötig, kein Build-Schritt.
  • Perfekt für Skripte, Worker, Cron-Ersatz.

Cons:

  • Kein Isolation: Der Prozess läuft direkt auf dem Host. Ein Buffer Overflow oder eine fehlerhafte Dependency kann den Dienst oder im schlimmsten Fall das System beeinflussen.
  • Abhängigkeiten sind “lose”: Du musst Node.js, Python oder Go selbst installieren und aktuell halten.
  • Reproduzierbarkeit: Eine Unit auf einem Server ist keine Garantie, dass sie auf dem nächsten identisch läuft.

Option B: Docker-Container

Du baust ein Image, definierst in einem Dockerfile oder Compose-File, was der Dienst braucht, und lässt Docker den Container verwalten.

Beispiel Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "server.js"]

Beispiel Compose-Fragment:

services:
  mein-dienst:
    build: .
    restart: always
    environment:
      - DATABASE_URL=${DATABASE_URL}
    volumes:
      - ./logs:/app/logs
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Pros:

  • Isolation: Der Container ist eine geschlossene Einheit. Der Host sieht nur einen Prozess mit eingeschränkten Rechten.
  • Reproduzierbarkeit: Das Image läuft überall gleich — auf dem Entwicklerlaptop, im CI, auf dem Produktionsserver.
  • Dependency-Free: Laufzeit, Bibliotheken, Tools — alles kommt aus dem Image.
  • Ökosystem: Compose, Portainer, Watchtower. Es gibt Tools für fast jeden Use Case.

Cons:

  • Overhead: Docker Daemon, Speicher-Layer, ggf. Rootless-Konfiguration.
  • Logging wird indirekt: docker logs oder ein Log-Treiber — journalctl ist nicht mehr direkt verfügbar.
  • Komplexität steigt: Image-Pflege, Base-Image-Updates, Layer-Caching, Security-Scans.
  • Bei einem einzelnen Host fühlt sich Docker manchmal wie ein Kanonenkuhflug an.

Entscheidung

Weg A: systemd.

Warum? Für diesen spezifischen Dienst auf diesem spezifischen Host ist die Isolation nicht das Killer-Feature, das sie in einem Multi-Tenant-Umfeld wäre. Der Dienst ist ein einfacher Node.js-Server ohne native Dependencies, der bereits auf dem Host läuft und stabil ist. Der Overhead einer Container-Lösung — Image-Build, Registry, Daemon — rechtfertigt sich nicht.

Ich akzeptiere den Trade-off: weniger Isolation, dafür weniger Komplexität und direkteren Zugriff auf das System. Solange ich den Dienst nicht an andere Teams abgebe oder horizontal skaliere, bleibt systemd die pragmatischere Wahl.

Implementierung

Die Umsetzung ist buchstäblich die Unit-Datei aus Option A plus ein systemctl daemon-reload && systemctl enable --now mein-dienst. Mehr nicht.

Für das Update-Verfahren reicht ein git pull im Arbeitsverzeichnis und ein systemctl restart mein-dienst. Kein Image-Build, kein Tagging, kein Push.

Fazit

Die Frage “systemd oder Docker” hat keine universelle Antwort — aber für einzelne Dienste auf einzelnen Servern mit bekannten Dependencies gewinnt systemd durch Einfachheit. Docker brilliert, wenn Isolation, Reproduzierbarkeit oder Multi-Host-Skalierung eine Rolle spielen.

Habt ihr ähnliche Erfahrungen gemacht? Wo habt ihr Container eingeführt, obwohl ein simpler systemd-Job gereicht hätte? Oder umgekehrt? Ich freue mich auf eure Fälle in den Kommentaren.