# 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