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
sudoprivileges. - Python 3, which Ubuntu 24.04 includes by default.
NoteOn a modern Ubuntu server, systemd can do most of what Supervisor does. Supervisor is still a good choice when you manage many similar programs, want to group and scale workers with one setting, or need to let a deploy script restart apps without writing unit files. If you only run one service, a systemd unit is equally valid.
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:
| Option | Purpose |
|---|---|
command | The program to run, with absolute paths. It must stay in the foreground; do not use a --daemon flag. |
directory, user | Working directory and the unprivileged user the process runs as. |
environment | Environment variables, as comma-separated KEY="value" pairs. |
autostart, autorestart | Start with Supervisor and restart whenever the process exits. |
startsecs | The process must stay up this many seconds to count as started. |
startretries | Attempts before Supervisor gives up and marks the program FATAL. |
stopasgroup, killasgroup | Send the stop signal to the whole process group, so Gunicorn's workers do not become orphans. |
redirect_stderr | Merge stderr into the stdout log. |
stdout_logfile_maxbytes, stdout_logfile_backups | Built-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
FATALorBACKOFFwithExited too quickly (process log may have details): the command dies withinstartsecs. Readsudo supervisorctl tail myapp:myapp-webor the file in/var/log/myapp/, then run the exactcommandas the same user (sudo -u myapp ...) from the same directory to see the error. spawn errorandcan't find command: the path incommandis wrong or not absolute. Supervisor does not use your shell'sPATH; 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 withsudo systemctl start supervisorand check/var/log/supervisor/supervisord.log.- Changes to a
.conffile have no effect: you edited the file but did not runrereadandupdate.restartalone reuses the old configuration. - Orphaned child processes after stopping: add
stopasgroup=trueandkillasgroup=trueto 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.
