Docker · Cronjobs · Worker · Supervisor · DevOps
Cronjobs und Worker in Docker richtig fahren
von Supervisor bis Graceful Shutdown

Wer Cronjobs und Worker-Prozesse unüberlegt in bestehende Docker-Container wirft, kämpft früher oder später mit PID-1-Zombies, verlorenen Signals und unkontrollierten Neustarts. Dedizierte Container, Supervisor und sauberes Signal-Handling ersetzen fragile Setups durch nachvollziehbare, produktionstaugliche Hintergrundprozesse.

15 Min. Lesezeit Supervisor · Cron · Worker · Signal-Handling · Health-Checks Docker Engine 24+ · Compose v2

1. Warum Cronjobs in Docker eine eigene Strategie brauchen

Wer Cronjobs aus einer klassischen Linux-Umgebung kennt, tippt zunächst die gewohnte Crontab in ein Dockerfile und wundert sich, warum der Prozess im Container nicht wie erwartet läuft. Der Grund liegt in der Natur von Docker-Containern: Ein Container ist konzipiert, um genau einen Hauptprozess zu betreiben. Der klassische Docker-Cronjob-Ansatz – einfach crond als Hintergrundprozess starten und daneben den eigentlichen Prozess laufen lassen – führt zu Problemen mit Signal-Weiterleitung, Zombie-Prozessen und unkontrollierbaren Neustarts.

Hinzu kommt der Kontext des Containers: Umgebungsvariablen, die über docker run -e oder Compose-Dateien übergeben werden, stehen dem crond-Daemon nicht zur Verfügung, weil Cron seine eigene, minimale Umgebung startet. Ein Docker-Worker oder -Cronjob muss diese Variablen explizit einlesen. In der Praxis entstehen so schwer reproduzierbare Fehler, bei denen Cronjobs lokal funktionieren, in der Staging-Umgebung aber schweigen. Die Lösung liegt in einem durchdachten Architekturansatz, der von Anfang an auf die Container-Natur eingeht.

2. Das PID-1-Problem und wie man es löst

In jedem Docker-Container übernimmt der erste gestartete Prozess die PID 1. Das ist normalerweise der Prozess, den CMD oder ENTRYPOINT im Dockerfile starten. PID 1 hat in Linux eine besondere Verantwortung: Sie muss Zombie-Kindprozesse einsammeln (Reaping) und Signals korrekt an Kindprozesse weiterleiten. Ein normales Shell-Skript oder ein Worker-Prozess, der nicht dafür ausgelegt ist, als Init-Prozess zu fungieren, erfüllt diese Aufgabe nicht. Das Ergebnis sind Zombie-Prozesse, die Ressourcen belegen, und Signals wie SIGTERM, die nicht bei den eigentlichen Worker-Prozessen ankommen.

Die sauberste Lösung für dieses Problem beim Betrieb von Docker-Cronjobs und -Workern ist tini. Tini ist ein minimaler Init-Prozess, der als PID 1 läuft, Zombie-Reaping durchführt und Signals korrekt weiterleitet. In modernen Docker-Versionen kann tini mit dem Flag --init beim docker run-Aufruf aktiviert werden. Alternativ kann tini direkt ins Image integriert werden. Eine weitere Option ist dumb-init von Yelp, das dieselben Aufgaben übernimmt und besonders in Python-basierten Worker-Images beliebt ist. Ohne einen dieser Helfer ist jedes Docker-Setup mit Hintergrundprozessen auf wackligem Fundament gebaut.


# Dockerfile for a worker container with tini as init process
FROM php:8.4-cli-alpine

# Install tini for correct PID-1 signal handling
RUN apk add --no-cache tini

# Copy worker script and set permissions
COPY worker.php /app/worker.php
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /app

# Use tini as PID 1 — ensures SIGTERM is forwarded and zombies are reaped
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/usr/local/bin/entrypoint.sh"]

3. Supervisor: mehrere Prozesse in einem Container steuern

Wenn mehrere eng verwandte Prozesse in einem Container laufen müssen – beispielsweise eine PHP-Anwendung und ein dazugehöriger Queue-Worker –, ist Supervisor das bewährte Werkzeug für Docker-Worker-Setups. Supervisor übernimmt die Rolle des Init-Prozesses, startet konfigurierte Programme, überwacht sie und startet sie bei Absturz automatisch neu. Die Konfiguration erfolgt in supervisord.conf-Dateien, die ins Docker-Image kopiert werden. Supervisor löst außerdem das PID-1-Problem, weil er korrekt als Init-Prozess agiert.

Die Konfiguration für einen Docker-Cronjob unter Supervisor sieht aus wie ein reguläres Programm mit command=, autostart=true, autorestart=true und definierten Restart-Delays. Für Queue-Worker, die endlos laufen sollen, eignet sich autorestart=unexpected – Supervisor startet den Worker neu, wenn er mit einem Fehlercode endet, aber nicht bei einem sauberen Exit (Exit-Code 0). Für Cronjobs hingegen, die nur einmalig ausgeführt werden und dann enden, empfiehlt sich eine separate Architektur ohne Supervisor, weil Supervisor keine native Cron-Syntax versteht.


# /etc/supervisor/conf.d/worker.conf
# Supervisor configuration for queue worker and cron in one container

[supervisord]
nodaemon=true
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid

[program:queue-worker]
# Queue worker — runs continuously, restarts on unexpected exit
command=php /app/bin/magento queue:consumers:start async.operations.all
directory=/app
autostart=true
autorestart=unexpected
startretries=3
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment=APP_ENV="production",WORKER_ID="%(process_num)s"
numprocs=2
process_name=%(program_name)s_%(process_num)02d

[program:cron-runner]
# Lightweight cron script — executes every minute via busybox cron
command=/usr/sbin/crond -f -l 8
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0

4. Dedizierte Worker-Container als bessere Alternative

Die Architektur mit dedizierten Docker-Worker-Containern ist in den meisten Fällen der Supervisor-Variante vorzuziehen. Statt mehrere Prozesse in einen Container zu quetschen, bekommt jeder Worker-Typ seinen eigenen Container. Das entspricht dem Docker-Prinzip der Single-Responsibility und ermöglicht es, Worker unabhängig zu skalieren, zu deployen und zu überwachen. Ein Queue-Worker für Email-Versand kann auf drei Instanzen skaliert werden, während ein Worker für PDF-Generierung auf einer Instanz bleibt – ohne dass beide Änderungen am selben Container-Image erfordern.

In einer Docker Compose-Konfiguration werden Docker-Worker als separate Services definiert, die dasselbe Basis-Image verwenden, aber unterschiedliche command-Werte haben. Das Basis-Image enthält den gesamten Anwendungscode, und der Einstiegspunkt entscheidet, welcher Prozess gestartet wird. Diese Struktur ermöglicht auch rollierende Updates einzelner Worker-Typen, ohne den Hauptanwendungscontainer neu starten zu müssen. In Kubernetes-Umgebungen entspricht das Deployment-Objekten mit unterschiedlichen Command-Werten.

5. Cron-Container: Cronjobs sauber verwalten

Für Docker-Cronjobs empfiehlt sich ein dedizierter Cron-Container, der ausschließlich den Cron-Daemon als PID 1 betreibt und die Cronjob-Definitionen als konfigurierbare Crontab-Datei enthält. Das wichtigste Detail dabei: Umgebungsvariablen aus dem Docker-Kontext müssen explizit an den Cron-Prozess weitergegeben werden. Das gelingt zuverlässig mit einem Entrypoint-Skript, das die aktuellen Umgebungsvariablen in eine Datei schreibt, die der Cron-Job dann per source lädt.

Alternativ gibt es spezialisierte Tools wie ofelia oder supercronic, die speziell für Docker-Cronjob-Setups entwickelt wurden. Supercronic ist ein Cron-Ersatz, der Container-freundliches Logging (JSON), korrekte Signal-Behandlung und eine explizite Fehlerbehandlung bietet. Ofelia verwaltet Cronjobs für alle Container in einem Docker-Compose-Stack über Labels, ähnlich wie Traefik das für HTTP-Routing macht. Beide Tools lösen die Umgebungsvariablen-Problematik von Haus aus und sind damit für produktive Docker-Worker- und Cron-Setups deutlich besser geeignet als nativer crond.


# docker-compose.yml — dedicated cron and worker services
version: "3.9"

services:
  app:
    image: myapp:latest
    environment:
      APP_ENV: production
      DB_HOST: db

  # Dedicated queue worker — scales independently from the app
  worker-queue:
    image: myapp:latest
    command: ["php", "bin/magento", "queue:consumers:start", "async.operations.all", "--max-messages=1000"]
    restart: unless-stopped
    depends_on:
      - app
    environment:
      APP_ENV: production
      DB_HOST: db
    deploy:
      replicas: 2

  # Dedicated cron container using supercronic
  cron:
    image: myapp:latest
    command: ["/usr/local/bin/supercronic", "/app/crontab"]
    restart: unless-stopped
    environment:
      APP_ENV: production
      DB_HOST: db
    volumes:
      - ./crontab:/app/crontab:ro

# /app/crontab (supercronic format — same as standard cron)
# */5 * * * * php /app/bin/magento indexer:reindex
# 0 2 * * *   php /app/bin/magento catalog:images:resize

6. Signal-Handling und Graceful Shutdown

Graceful Shutdown ist bei Docker-Worker-Prozessen einer der häufigsten Schwachpunkte in produktiven Setups. Wenn Docker einen Container stoppt, sendet die Engine zunächst SIGTERM an PID 1 und wartet standardmäßig zehn Sekunden. Nimmt der Prozess das Signal nicht an oder leitet er es nicht weiter, folgt SIGKILL. Ein Worker, der gerade einen wichtigen Job bearbeitet, wird damit abrupt unterbrochen. Das kann zu inkonsistenten Datenbankzuständen, doppelt verarbeiteten Jobs oder verlorenen Meldungen führen.

Die Lösung besteht darin, den Docker-Worker-Prozess so zu implementieren, dass er SIGTERM abfängt, laufende Jobs fertig bearbeitet und dann sauber beendet. In PHP wird das mit pcntl_signal(SIGTERM, function() { $this->shouldStop = true; }) und einem entsprechenden Check in der Worker-Schleife umgesetzt. In der Compose- oder Kubernetes-Konfiguration kann außerdem stop_grace_period auf einen höheren Wert gesetzt werden – beispielsweise 30 oder 60 Sekunden –, damit lang laufende Jobs Zeit haben, zu einem sauberen Haltepunkt zu kommen, bevor SIGKILL erzwungen wird.

7. Health-Checks für Worker und Cron-Container

Ohne Health-Checks weiß Docker nicht, ob ein Docker-Worker-Container wirklich arbeitet oder in einem Deadlock feststeckt. Der Container-Status zeigt running, aber der eigentliche Worker-Prozess kann hängen oder längst abgestürzt sein. Docker-Health-Checks erlauben es, regelmäßig einen Befehl im Container auszuführen und den Container als unhealthy zu markieren, wenn der Befehl fehlschlägt. Orchestratoren wie Kubernetes oder Docker Swarm können unhealthy Container automatisch neu starten.

Für Docker-Cronjob-Container ist ein heartbeat-basierter Health-Check das geeignete Muster: Der Cronjob schreibt bei jeder erfolgreichen Ausführung einen Zeitstempel in eine Datei. Der Health-Check prüft, ob der Zeitstempel nicht älter als das doppelte Cron-Intervall ist. So wird ein hängender oder gestorbener Cron-Prozess erkannt und der Container neu gestartet. Für Worker mit Supervisor kann der Health-Check den Supervisor-Status per supervisorctl status abfragen und auf RUNNING prüfen.


# Dockerfile — health check for a queue worker container
FROM php:8.4-cli-alpine
RUN apk add --no-cache tini

COPY worker.sh /usr/local/bin/worker.sh
COPY healthcheck.sh /usr/local/bin/healthcheck.sh
RUN chmod +x /usr/local/bin/worker.sh /usr/local/bin/healthcheck.sh

# Worker writes a heartbeat timestamp on each successful iteration
# Health check verifies the heartbeat is not older than 120 seconds
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
  CMD /usr/local/bin/healthcheck.sh

ENTRYPOINT ["/sbin/tini", "--"]
CMD ["/usr/local/bin/worker.sh"]

# --- healthcheck.sh ---
# #!/bin/sh
# HEARTBEAT_FILE="/tmp/worker_heartbeat"
# MAX_AGE=120
# if [ ! -f "$HEARTBEAT_FILE" ]; then exit 1; fi
# AGE=$(( $(date +%s) - $(stat -c %Y "$HEARTBEAT_FILE") ))
# [ "$AGE" -lt "$MAX_AGE" ] || exit 1

8. Logging aus Worker-Containern sauber aggregieren

Ein häufig unterschätzter Aspekt beim Betrieb von Docker-Workern und Cron-Containern ist das Logging. Im Gegensatz zu HTTP-Request-Handlern erzeugen Worker-Prozesse kontinuierlich Log-Einträge, die ohne strukturiertes Logging schnell unübersichtlich werden. Das Docker-Log-Modell erwartet, dass Prozesse nach stdout und stderr schreiben – alle anderen Log-Ziele (Dateien, Syslog) müssen explizit weitergeleitet werden. Der einfachste Ansatz ist JSON-strukturiertes Logging direkt nach stdout, das dann von Docker gesammelt und an Log-Treiber wie Loki, Elasticsearch oder CloudWatch weitergeleitet wird.

Bei mehreren Docker-Worker-Instanzen ist die Korrelation von Log-Einträgen über Container-IDs und Worker-IDs hinweg entscheidend. Jeder Log-Eintrag sollte die Container-ID, einen Worker-Identifier, den Job-Typ und eine Trace-ID für den verarbeiteten Job enthalten. In der Praxis wird das über Umgebungsvariablen gelöst: Die Compose-Konfiguration setzt WORKER_ID, der Worker-Prozess liest den Wert und hängt ihn an alle Log-Einträge. So lassen sich bei Problemen alle Einträge zu einem bestimmten Job-Lauf in Grafana oder Kibana filtern.

9. Strategien im direkten Vergleich

Die Wahl der Architektur für Docker-Worker und Cronjobs hängt vom jeweiligen Anwendungsfall ab. Jeder Ansatz hat spezifische Stärken und Schwächen, die man beim Design des Systems kennen sollte.

Strategie Vorteil Nachteil Empfehlung
Cron im App-Container Einfache Einrichtung Env-Vars fehlen, Zombie-Risiko Nur für lokale Entwicklung
Supervisor Mehrere Prozesse, Auto-Restart Kein natives Cron, komplexer Eng gekoppelte Worker-Typen
Dedizierter Worker-Container Unabhängig skalierbar, klar Mehr Services in Compose Produktions-Empfehlung
Supercronic JSON-Log, korrektes Signal-Handling Nur für Cron-Workloads Cron-Container Empfehlung
Kubernetes CronJob Native K8s-Integration Setzt Kubernetes voraus Ab mittlerer Skalierung

Die Entscheidung zwischen Supervisor und dedizierten Containern fällt in den meisten Produktionsumgebungen zugunsten dedizierter Container aus, weil sie die operativen Vorteile von Docker vollständig nutzen: unabhängige Deployments, unabhängige Skalierung und klare Verantwortlichkeiten. Supercronic und tini sind in beiden Varianten sinnvoll und lösen die technischen Grundprobleme – PID-1, Signal-Handling und Logging – zuverlässig.

Mironsoft

Docker-Infrastruktur, Worker-Architekturen und Produktions-Deployments

Docker-Worker und Cronjobs die wirklich laufen?

Wir analysieren bestehende Worker-Setups, identifizieren fragile Konfigurationen und bauen produktionstaugliche Docker-Worker- und Cron-Architekturen mit korrektem Signal-Handling, Health-Checks und strukturiertem Logging.

Worker-Architektur

Dedizierte Container, Supervisor-Config und Skalierungskonzepte für Queue-Worker

Signal-Handling

Graceful Shutdown, tini-Integration und korrekte PID-1-Konfiguration

Monitoring

Health-Checks, Heartbeat-Monitoring und Log-Aggregation für Hintergrundprozesse

10. Zusammenfassung

Docker-Cronjobs und Worker richtig zu betreiben, bedeutet zunächst das PID-1-Problem mit tini oder dumb-init zu lösen, damit Signals korrekt weitergeleitet werden und Zombie-Prozesse nicht entstehen. Dedizierte Container pro Worker-Typ sind der sauberere Ansatz gegenüber mehreren Prozessen in einem Container, weil sie unabhängige Skalierung, unabhängige Deployments und klare Verantwortlichkeiten ermöglichen. Supercronic löst die Umgebungsvariablen-Problematik von Cron in Docker eleganter als nativer crond.

Graceful Shutdown durch explizites Signal-Handling im Worker-Code verhindert abrupt unterbrochene Jobs und inkonsistente Systemzustände. Health-Checks auf Basis von Heartbeat-Timestamps machen den echten Betriebszustand eines Docker-Worker-Containers für Orchestratoren sichtbar. Strukturiertes Logging nach stdout mit Worker-ID und Job-Trace-ID ermöglicht die Fehlersuche in produktiven Setups mit mehreren parallelen Worker-Instanzen.

Docker-Cronjobs und Worker — Das Wichtigste auf einen Blick

PID-1-Problem

Tini oder dumb-init als PID 1 verwenden – korrekte Signal-Weiterleitung und Zombie-Reaping ohne eigenen Init-Code im Worker.

Dedizierte Container

Ein Worker-Typ pro Container – unabhängig skalierbar, unabhängig deploybar, klare Verantwortlichkeiten im Compose-Stack.

Supercronic für Cron

Container-freundlicher Cron-Ersatz mit JSON-Logging, korrektem Signal-Handling und nativer Env-Var-Unterstützung.

Health-Checks

Heartbeat-Timestamp + HEALTHCHECK-Direktive – Orchestrator erkennt hängende Worker und startet Container automatisch neu.

11. FAQ: Cronjobs und Worker in Docker

1Warum sehen Docker-Cronjobs keine Env-Vars?
Cron startet mit minimaler Umgebung. Entrypoint-Skript muss aktuelle Vars in /etc/environment schreiben oder Supercronic verwenden, das Env-Vars nativ unterstützt.
2Was ist das PID-1-Problem?
PID 1 muss Zombie-Reaping und Signal-Weiterleitung leisten. Normale Worker tun das nicht. Tini oder dumb-init als PID 1 lösen das Problem zuverlässig.
3Supervisor vs. dedizierte Container?
Dedizierte Container sind für Produktion vorzuziehen – unabhängig skalierbar und deploybar. Supervisor nur wenn Prozesse unbedingt zusammengehören.
4Was macht Supercronic besser als crond?
Läuft im Vordergrund, hat JSON-Logging, leitet Signals korrekt weiter und unterstützt Env-Vars nativ. Crond daemonisiert sich selbst und bricht das Docker-Prozessmodell.
5Graceful Shutdown für PHP-Worker?
pcntl_signal(SIGTERM, ...) abfangen, shouldStop-Flag setzen, nach jedem Job prüfen. stop_grace_period in Compose auf 30-60 Sekunden erhöhen.
6Wie viele Worker-Instanzen starten?
Mit 1 beginnen, Queue-Aufstau beobachten, dann auf 2-4 skalieren. deploy.replicas in Compose oder HPA in Kubernetes für automatische Skalierung nutzen.
7Health-Check für Cron-Container?
Cron schreibt Timestamp nach erfolgreicher Ausführung. Health-Check prüft ob Alter < 2× Cron-Intervall. Bei Überschreitung: unhealthy, Container wird neu gestartet.
8Docker-Cron mit Kubernetes CronJob ersetzen?
Ja, in Kubernetes-Umgebungen bevorzugt. Native Job-Verwaltung, Historienkonfiguration, parallele und sequenzielle Ausführungsrichtlinien inklusive.
9Logs aus mehreren Worker-Containern aggregieren?
JSON nach stdout mit Worker-ID und Job-Trace-ID. Docker-Log-Treiber leitet an Loki oder Elasticsearch. In Grafana nach WORKER_ID filtern.
10Cronjob-Mehrfachausführung verhindern?
Supercronic mit overlapping=false. Oder flock im Skript. In Kubernetes CronJob mit concurrencyPolicy: Forbid. DB-Locks für verteilte Systeme.