82 lines
3.0 KiB
Plaintext
82 lines
3.0 KiB
Plaintext
# Two processes in one container, and the rules that make that survivable.
|
|
#
|
|
# A container with two processes needs something that reaps them, restarts
|
|
# them, and — most importantly — dies when either one dies for good. Without
|
|
# that last part the container stays "up" with half an app in it, the
|
|
# orchestrator sees a healthy container, and the only symptom is that every
|
|
# request 502s.
|
|
|
|
[supervisord]
|
|
nodaemon=true
|
|
user=root
|
|
logfile=/dev/stdout
|
|
logfile_maxbytes=0
|
|
pidfile=/tmp/supervisord.pid
|
|
|
|
# Without these three, `supervisorctl` inside the container refuses to run at
|
|
# all — "ini file does not include supervisorctl section" — so the one tool for
|
|
# asking which half of the app is down, or restarting it without killing the
|
|
# container, is unavailable exactly when it is needed. The socket is a file in
|
|
# the container with no port and no network listener, so this costs nothing.
|
|
[unix_http_server]
|
|
file=/tmp/supervisor.sock
|
|
chmod=0700
|
|
|
|
[supervisorctl]
|
|
serverurl=unix:///tmp/supervisor.sock
|
|
|
|
[rpcinterface:supervisor]
|
|
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
|
|
|
|
[program:backend]
|
|
command=uvicorn main:app --host 127.0.0.1 --port 8001 --root-path /api
|
|
directory=/app/backend
|
|
autostart=true
|
|
autorestart=true
|
|
# Three failures in a row is a real failure, not a slow start. Retrying forever
|
|
# hides a crash-looping app behind a container that never exits.
|
|
startretries=3
|
|
startsecs=5
|
|
priority=10
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
# Python buffers stdout when it is a pipe, and supervisord gives it a pipe.
|
|
# Without this the app's logs appear only when the buffer fills — which for a
|
|
# crash means never, and the traceback that would explain it is lost.
|
|
environment=PYTHONUNBUFFERED="1"
|
|
|
|
[program:nginx]
|
|
command=nginx -g "daemon off;"
|
|
autostart=true
|
|
autorestart=true
|
|
startretries=3
|
|
# Started after the backend so the first proxied request has somewhere to go.
|
|
# It is only ordering, not waiting: nginx starts whether or not the backend is
|
|
# ready, which is correct — it serves the frontend meanwhile.
|
|
priority=20
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|
|
|
|
[eventlistener:quit_on_failure]
|
|
# The part that matters. When either program enters FATAL — supervisord has
|
|
# given up restarting it — this stops supervisord, so the container exits and
|
|
# whatever supervises it can react. Otherwise the container lives on with one
|
|
# half of the app missing: healthy from outside, a total outage from a browser.
|
|
#
|
|
# A real script rather than an inline `sh -c`, because the listener protocol is
|
|
# a length-prefixed exchange and getting it wrong does not raise — it leaves a
|
|
# listener reading forever while the thing it watches stays broken. See the
|
|
# script's own docstring; the shell version of this genuinely did that.
|
|
command=python3 -u /app/quit_on_failure.py
|
|
events=PROCESS_STATE_FATAL
|
|
autostart=true
|
|
autorestart=true
|
|
stdout_logfile=/dev/stdout
|
|
stdout_logfile_maxbytes=0
|
|
stderr_logfile=/dev/stderr
|
|
stderr_logfile_maxbytes=0
|