Supervisor is a process control system that starts your long-running programs, restarts them when they crash and collects their output into log files. It is popular for Python web apps, queue workers and any command that was never written to run as a daemon. In this tutorial you will install Supervisor on Ubuntu 24.04, run a small Gunicorn web application and a pool of background workers under it, and learn the supervisorctl commands you will use day to day.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
  • A non-root user with sudo privileges.
  • Python 3, which Ubuntu 24.04 includes by default.

Step 1 - Installing Supervisor

Install the package from the Ubuntu repository:

sudo apt update
sudo apt install supervisor

The package creates a systemd service called supervisor that runs the supervisord daemon, and enables it at boot. Check that it is running:

sudo systemctl status supervisor
● supervisor.service - Supervisor process control system for UNIX
     Loaded: loaded (/usr/lib/systemd/system/supervisor.service; enabled; preset: enabled)
     Active: active (running) since ...

Ask the daemon which programs it manages. The list is empty for now, so the command prints nothing:

sudo supervisorctl status

supervisorctl talks to the daemon through the socket /var/run/supervisor.sock, which only root can use by default. That is why every supervisorctl command in this guide uses sudo.

Step 2 - Understanding the configuration layout

The main file is /etc/supervisor/supervisord.conf. You rarely need to change it: at the end it includes every file in /etc/supervisor/conf.d/ that ends in .conf:

tail -n 3 /etc/supervisor/supervisord.conf
[include]
files = /etc/supervisor/conf.d/*.conf

The convention is one file per application in /etc/supervisor/conf.d/, each containing one or more [program:name] sections. Supervisor's own log is /var/log/supervisor/supervisord.log.

Step 3 - Preparing an example application

To have something real to manage, create a minimal Python web application served by Gunicorn, running as its own unprivileged user.

Create a system user and the application directory:

sudo adduser --system --group --home /opt/myapp myapp

Install the venv module and create a virtual environment with Gunicorn:

sudo apt install python3-venv
sudo -u myapp python3 -m venv /opt/myapp/venv
sudo -u myapp /opt/myapp/venv/bin/pip install gunicorn

Create the application file:

sudo -u myapp nano /opt/myapp/app.py
import os


def application(environ, start_response):
    body = f"Hello from {os.environ.get('APP_ENV', 'unknown')}\n".encode()
    start_response("200 OK", [("Content-Type", "text/plain"), ("Content-Length", str(len(body)))])
    return [body]

Create a directory for the application logs:

sudo mkdir -p /var/log/myapp
sudo chown myapp:myapp /var/log/myapp

Before handing the command to Supervisor, run it once by hand to make sure it works:

sudo -u myapp /opt/myapp/venv/bin/gunicorn --chdir /opt/myapp --bind 127.0.0.1:8000 app:application
[INFO] Starting gunicorn 23.0.0
[INFO] Listening at: http://127.0.0.1:8000

Press Ctrl+C to stop it.

Step 4 - Creating a program configuration

Create a configuration file for the web application:

sudo nano /etc/supervisor/conf.d/myapp.conf
[program:myapp-web]
command=/opt/myapp/venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 app:application
directory=/opt/myapp
user=myapp
environment=APP_ENV="production"
autostart=true
autorestart=true
startsecs=5
startretries=3
stopsignal=TERM
stopwaitsecs=30
stopasgroup=true
killasgroup=true
redirect_stderr=true
stdout_logfile=/var/log/myapp/web.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5

What the important options do:

OptionPurpose
commandThe program to run, with absolute paths. It must stay in the foreground; do not use a --daemon flag.
directory, userWorking directory and the unprivileged user the process runs as.
environmentEnvironment variables, as comma-separated KEY="value" pairs.
autostart, autorestartStart with Supervisor and restart whenever the process exits.
startsecsThe process must stay up this many seconds to count as started.
startretriesAttempts before Supervisor gives up and marks the program FATAL.
stopasgroup, killasgroupSend the stop signal to the whole process group, so Gunicorn's workers do not become orphans.
redirect_stderrMerge stderr into the stdout log.
stdout_logfile_maxbytes, stdout_logfile_backupsBuilt-in log rotation: 5 files of 10 MB each.

Step 5 - Loading and starting the program

Supervisor does not watch its configuration directory. After adding or changing a file, tell it to read the files and apply the differences:

sudo supervisorctl reread
sudo supervisorctl update
myapp-web: available
myapp-web: added process group

reread only reports what changed. update starts new programs, restarts changed ones and removes deleted ones, without touching the rest. Check the status:

sudo supervisorctl status
myapp-web                        RUNNING   pid 4127, uptime 0:00:08

Test the application:

curl http://127.0.0.1:8000
Hello from production

Verifying automatic restarts

Simulate a crash by killing the Gunicorn master process, then check the status again:

sudo kill -9 "$(sudo supervisorctl pid myapp-web)"
sudo supervisorctl status myapp-web
myapp-web                        RUNNING   pid 4163, uptime 0:00:02

The new PID and the reset uptime show that Supervisor restarted the application on its own.

Step 6 - Running a pool of workers

Background workers (queue consumers, Celery or RQ workers, scrapers) often need several identical copies. Supervisor handles this with numprocs.

Create a simple worker script that stands in for a real queue consumer:

sudo -u myapp nano /opt/myapp/worker.py
import os
import signal
import sys
import time

running = True


def stop(signum, frame):
    global running
    running = False


signal.signal(signal.SIGTERM, stop)

while running:
    print(f"worker {os.getpid()} processing jobs", flush=True)
    time.sleep(10)

print(f"worker {os.getpid()} stopped cleanly", flush=True)
sys.exit(0)

The script handles SIGTERM and finishes its current loop before exiting, which is how a real worker should behave so it does not drop jobs on restart. The flush=True makes output reach the log immediately.

Append a worker program and a group to /etc/supervisor/conf.d/myapp.conf:

sudo nano /etc/supervisor/conf.d/myapp.conf
[program:myapp-worker]
command=/opt/myapp/venv/bin/python worker.py
process_name=%(program_name)s_%(process_num)02d
numprocs=3
directory=/opt/myapp
user=myapp
autostart=true
autorestart=true
stopwaitsecs=60
redirect_stderr=true
stdout_logfile=/var/log/myapp/worker_%(process_num)02d.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5

[group:myapp]
programs=myapp-web,myapp-worker

With numprocs, process_name must include %(process_num), so each copy gets a unique name. stopwaitsecs=60 gives each worker a minute to finish its current job before Supervisor sends SIGKILL. The [group:myapp] section lets you control the web process and all workers together.

Apply the changes:

sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl status
myapp:myapp-web                  RUNNING   pid 4210, uptime 0:00:06
myapp:myapp-worker_00            RUNNING   pid 4211, uptime 0:00:06
myapp:myapp-worker_01            RUNNING   pid 4212, uptime 0:00:06
myapp:myapp-worker_02            RUNNING   pid 4213, uptime 0:00:06

Once a program belongs to a group, refer to it as group:program, for example myapp:myapp-web.

Step 7 - Everyday supervisorctl commands

These are the commands you will use most, for example in a deployment script:

sudo supervisorctl restart myapp:*

Restarts every program in the myapp group after you deploy new code.

sudo supervisorctl stop myapp:myapp-worker_01
sudo supervisorctl start myapp:myapp-worker_01

Stops and starts a single process.

sudo supervisorctl tail -f myapp:myapp-worker_00

Follows a process's log in real time, like tail -f. Press Ctrl+C to stop.

sudo supervisorctl tail -2000 myapp:myapp-web

Shows the last 2000 bytes of the log.

You can also run sudo supervisorctl with no arguments to open an interactive shell where you type the same commands without the prefix; type help to list them.

Troubleshooting

  • Status shows FATAL or BACKOFF with Exited too quickly (process log may have details): the command dies within startsecs. Read sudo supervisorctl tail myapp:myapp-web or the file in /var/log/myapp/, then run the exact command as the same user (sudo -u myapp ...) from the same directory to see the error.
  • spawn error and can't find command: the path in command is wrong or not absolute. Supervisor does not use your shell's PATH; use full paths such as /opt/myapp/venv/bin/gunicorn.
  • unix:///var/run/supervisor.sock no such file: the daemon is not running. Start it with sudo systemctl start supervisor and check /var/log/supervisor/supervisord.log.
  • Changes to a .conf file have no effect: you edited the file but did not run reread and update. restart alone reuses the old configuration.
  • Orphaned child processes after stopping: add stopasgroup=true and killasgroup=true to the program so signals reach its children.

Conclusion

Supervisor now keeps a Gunicorn web application and three background workers running on Ubuntu 24.04, restarts them when they fail, rotates their logs and lets you control them as one group. Next, put Nginx in front of the application on port 80 or 443 as a reverse proxy to 127.0.0.1:8000, replace the example worker with your real queue consumer, and call supervisorctl restart myapp:* from your deployment pipeline.