Skip to main content
Roman's tech blog

Lightweight docker process supervisor

As mentioned in a previous post, I used to use supervisord service to run several daemon processes continuously.

did the job, but felt like overkill. It is written in Python and provides a lot of functionality I don't really need.

All I actually need are two things:

  1. Restart an application if it crashes or exits.
  2. Capture the application's output and send it to stdout.

It turns out this can easily be done with a simple Bash script:

#!/bin/bash

# ==========================
# Configuration: list of services
# Each entry: "name command"
# ==========================
services=(
  "imapfilter-my-super-mailbox /usr/bin/imapfilter -c /path-to-config/_main.lua"
  "http-api-server gunicorn -w 1 -k gthread --threads 1 --preload my-super-server:app --log-level error --bind 0.0.0.0:8888"
#  "unstable-service sleep 10"
)

# Array to keep track of PIDs
pids=()

# Function to supervise a single service
run_service() {
  local name="$1"
  shift
  local cmd="$@"

  while true; do
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting $name..."

    cd /jobs/bin
    # Run command and wait for it
    $cmd
    exit_code=$?

    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $name exited with code $exit_code. Restarting..."
    sleep 3  # optional backoff
  done
}

# Function to stop all background services gracefully
shutdown() {
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] Caught termination signal. Stopping services..."
  for pid in "${pids[@]}"; do
    kill "$pid" 2>/dev/null || true
  done
  wait
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] All services stopped. Exiting."
  exit 0
}

# Trap termination signals
trap shutdown SIGTERM SIGINT

# Start all services in background
for svc in "${services[@]}"; do
  name=$(echo $svc | awk '{print $1}')
  cmd=$(echo $svc | cut -d' ' -f2-)

  run_service "$name" $cmd &
  pids+=($!)
done

# Wait for all background services
wait

Run it with tini, and you have a container that can run your custom daemons:

FROM debian:trixie-slim

RUN apt-get update \
 && apt-get install -y --no-install-recommends \
    tini ca-certificates curl bc \
    python3 python3-requests \
    imapfilter \
    lego \
    gunicorn python3-flask  \
    python3-feedparser python3-feedgen \
    python3-websocket \
    python3-redis \
 && apt-get clean && rm -rf /var/lib/apt/lists/*

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/my/path/to/supervisor.sh"]

The only feature missing for now is log separation by process. The services I am running are stable enough that I haven't bothered implementing it.