Gimme more: Mehrere Prozesse im Docker-Container mit supervisor

Applikationscontainer sind vor allem als isoliert laufende Prozesse zu sehen, weshalb der Vergleich mit virtuellen Maschinen stark hinkt. Nichtsdestotrotz gibt es Szenarien, in denen mehr als ein Prozess im Container Sinn macht. supervisor als selbsternanntes „Process Control System“ hilft hier weiter.

supervisor kommt aus der Prä-Container-Ära und hilft bei der Verwendung simpler Programme, die vielleicht kein eigenes Prozess-Management mitbringen. Damit lässt sich beispielsweise die Ausgabe eines Prozesses fangen und diese in ein Logfile schreiben oder wacklige Tools können damit neu gestartet werden. SIGTERMS zu den Subprozessen werden auch weitergeleitet. Wird supervisord angewiesen sich zu beenden, leitet es dies also an seine Subprozesse weiter.

Die Einsatzmöglichkeiten sind vielfältig, wie ein kurzer Blick in die Dokumentation zeigt. Weitere Tools in dem Bereich sind runit und S6. Ich greife gern auf supervisor zurück, weil es in den meisten Distributionen enthalten ist und damit schnell installiert und auch eingerichtet ist. Wer es leichtgewichtiger mag, kann auch über ein Bash-Skript nachdenken, wie es in der offiziellen Docker-Doku skizziert wird.

OK, wie machen wir's?

Ziel soll es sein, PHP zusammen mit einem Cronjob laufen zu lassen. Legen wir los und betrachten das folgende Dockerfile.

FROM php:7.2-fpm-alpine
LABEL maintainer "Patrick Baber <patrick.baber@ueber.io>"

# Install supervisor
RUN apk --update add supervisor

# Configure supervisor
COPY supervisord.conf /etc/supervisor/supervisord.conf

# Configure cron
COPY crontab /etc/cron/crontab

# Init cron
RUN crontab /etc/cron/crontab

# Run supervisor
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]

Uns soll zunächst ein einfaches PHP-Image als Fundament dienen. Dann installieren wir das Paket supervisor und kopieren dafür die Konfigurationsdatei. Es folgt alles Notwendige für cron. Was das ist, kann meinem vorigen Artikel entnommen werden.

Abschließend definieren wir das Kommando, welches später im Container laufen soll, dem wir den Pfad zur Konfigurationsdatei übergeben.

Spannend wird nun der Inhalt der Datei supervisord.conf, die im selben Verzeichnis abgelegt ist.

[supervisord]
logfile = /dev/null
loglevel = info
pidfile = /var/run/supervisord.pid
nodaemon = true

;[include]
;files = /etc/supervisor/conf.d/*.conf

[program:php-fpm]
command = php-fpm
autostart = true
autorestart = true
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr
stderr_logfile_maxbytes = 0

[program:crond]
command = crond -f
autostart = true
autorestart = true
stdout_logfile = /dev/stdout
stdout_logfile_maxbytes = 0
stderr_logfile = /dev/stderr
stderr_logfile_maxbytes = 0

Die Syntax entspricht einer klassischen .ini-Datei, die über Sections in eckigen Klammern strukturiert wird.

[supervisord]

In der ersten Section wird der Daemon (supervisord) konfiguriert, den wir bereits in der ersten Zeile dazu anleiten kein Daemon zu sein, damit der Prozess im Vordergrund läuft und seine Ausgaben nach stdout schreibt, wie man sich das bei Docker-Containern wünscht. Ein zusätzliches Logfile benötigen wir daher nicht, weshalb wir es mit logfile = /dev/null deaktivieren.

[include]

Wer sich mehr Struktur wünscht, kann seine Konfiguration auch in mehrere Dateien aufteilen, was gerade bei der Verwendung vieler Prozesse innerhalb des Containers Sinn macht, allerdings führt man damit das Prinzip der isolierten Prozesse ad absurdum. Die include-Anweisung ist dennoch recht nützlich, weshalb ich sie auskommentiert hier aufgenommen habe.

[program:XYZ]

Hier sind wir nun beim spannenden Teil angelangt. Wir können nun für jedes Programm, einen eigenen Abschnitt definieren, der als zentrale Eigenschaft vor allem das Kommando zum Starten des Prozesses benötigt. Man merkt, ich verwende Programm und Prozess hier synonym. Vorbereitet ist eine Sektion für php-fpm, was im offiziellen PHP-Image von dem wir ableiten, der einzige Prozess wäre, gefolgt von einem Abschnitt für den zweiten Subprozess crond -f. Ich habe hier keinerlei crontab, die den Cronjob auch nur eine Sache ausführen lassen würde, aber dies soll nur als Beispiel dienen. Wer einen vollwärtigen Cronjob nutzen möchte, schaut bitte in diesen Artikel. autorestart startet die Anwendung neu, ob sie bewusst oder unbewusst heruntergefahren wurde. Es folgen nun Einstellungen, welche die Programmausgabe von stdout und stderr direkt an Docker weiterleiten.

Bevor wir uns ans Bauen des Images machen können, benötigen wir noch die crontab, die in der Realität eher ein PHP-Skript asychron zum herkömmlichen Aufruf per FastCGI ausführt. Hier übernehme ich einfach die Crontab aus meinem vorigen Artikel, die für eine einfache Shell-Ausgabe sorgt. Zur Veranschaulichung unseres Vorhabens soll das aber reichen.

* * * * * echo "Hello world" >> /dev/stdout 2>&1
# crontab requires empty line at end of file

Build 'n' Run

Das Image kann nun gebaut und der Container gestartet werden.

# Build
docker image build -t supervisor-image .

# Run
docker container run --rm --name supervisor-container supervisor-image

Es folgt die Ausgabe des Containers.

2017-12-11 16:17:50,592 CRIT Supervisor running as root (no user in config file)
2017-12-11 16:17:50,593 INFO supervisord started with pid 1
2017-12-11 16:17:51,597 INFO spawned: 'php-fpm' with pid 9
2017-12-11 16:17:51,599 INFO spawned: 'crond' with pid 10
[11-Dec-2017 16:17:51] NOTICE: fpm is running, pid 9
[11-Dec-2017 16:17:51] NOTICE: ready to handle connections
2017-12-11 16:17:52,624 INFO success: php-fpm entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
2017-12-11 16:17:52,624 INFO success: crond entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
Hello world
Hello world

Die erste Meldung lässt einen direkt unangenehm aufstoßen. Sie lässt sich vermeiden, indem man supervisord durch einen anderen Benutzer startet. Die Rechte des Nutzers müssen allerdings für die Tätigkeit des Subprozesses reichen. Um beispielsweise einen Low-Port (< 1024) zu öffnen, braucht es root-Rechte oder zumindest die entsprechende Capability aus dem Kernel. Das Thema ist zu groß, um es in diesem Artikel zu behandelt, allerdings möchte ich darauf hinweisen.

Darüber hinaus entnehmen wir dem Log, dass supervisord korrekt startet und seine Subprozesse ebenfalls. Anschließend produziert PHP auch die erste Ausgabe - zu erkennen an der anderen Datumsformatierung. Das ist nicht ideal. Falls jemand weiß, wie man das eleganter lösen kann, dabei aber trotzdem die Infos bei Docker ankommen, gibt Bescheid.

Zusätzlich gibt es noch die Info von supervisord, dass es zufrieden mit den beiden Prozessen ist und ihren Zustand entsprechend als RUNNING definiert.

Nach spätestens einer Minute meldet sich dann auch crond mit seiner Testausgabe „Hello world“.

Die beiden Prozesse verrichten also, wie gewünscht, ihren Dienst. Ist also alles schnell umgesetzt. Viel Spaß mit mehreren Prozessen im Container!

Patrick Baber

Als erfahrener Programmierer löst Patrick jeden Gordischen Knoten, findet die geeignete Methode und entwickelt damit flexible Software-Systeme.