Client Area

Supervisord: Running Python, PHP, and Node Processes on a VPS

ByDomain India Team
9 min read22 Apr 20262 views

In this article

  • 1Why a process manager?
  • 2Install
  • 3Configure your first program
  • 4Apply the config
  • 5Day-to-day commands

Supervisord: Running Python, PHP, and Node Processes on a VPS

PM2 is the Node.js favourite. Supervisord is the cross-language equivalent — a process manager that keeps Python, PHP, Ruby, and shell-script workers alive on a Linux server. Widely used for Laravel queue workers, Celery workers, and custom long-running daemons. This guide covers install, config, auto-restart on crash, log rotation, and the common patterns.

Why a process manager?

Anything you run with python worker.py & dies when:

  • You close the SSH session
  • The server reboots
  • The process crashes (unhandled exception, OOM)
  • You run killall python by accident

A process manager owns the lifecycle — starts on boot, restarts on crash, logs stdout / stderr, exposes a control interface.

Supervisord's niche: language-agnostic, simple configuration, clean multi-process grouping. Mature and widely deployed.

Install

Ubuntu / Debian:

bash
sudo apt install supervisor

AlmaLinux / CentOS:

bash
sudo dnf install supervisor

Verify the service:

bash
sudo systemctl status supervisor      # Debian
sudo systemctl status supervisord     # RHEL family

Enable on boot:

bash
sudo systemctl enable supervisor

Configure your first program

Supervisord reads program configs from /etc/supervisor/conf.d/*.conf (Debian) or /etc/supervisord.d/*.ini (RHEL). Create one file per program:

ini
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/myapp/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
directory=/home/myapp
autostart=true
autorestart=true
user=myappuser
numprocs=4
redirect_stderr=true
stdout_logfile=/var/log/laravel-worker.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
stopwaitsecs=3600

Breakdown of the key fields:

  • process_name — unique name per spawned process (%(process_num)02d gives 00, 01, 02, 03 suffixes)
  • command — the exact command to run, with full path to the executable
  • directory — working directory
  • autostart=true — start when supervisor starts
  • autorestart=true — restart if it exits (for any reason)
  • user — run as this non-root user (NEVER run workers as root)
  • numprocs=4 — spawn 4 instances for concurrency
  • redirect_stderr=true — merge stderr into stdout (easier log handling)
  • stdout_logfile — where to write logs
  • stdout_logfile_maxbytes / stdout_logfile_backups — log rotation
  • stopwaitsecs=3600 — on stop, wait up to 1 hour for graceful shutdown (don't kill mid-job)

Apply the config

bash
sudo supervisorctl reread      # scan for new config files
sudo supervisorctl update      # apply changes
sudo supervisorctl status      # see all programs

You should see:

laravel-worker:laravel-worker_00   RUNNING   pid 12345, uptime 0:00:05
laravel-worker:laravel-worker_01   RUNNING   pid 12346, uptime 0:00:05
laravel-worker:laravel-worker_02   RUNNING   pid 12347, uptime 0:00:05
laravel-worker:laravel-worker_03   RUNNING   pid 12348, uptime 0:00:05

Day-to-day commands

bash
sudo supervisorctl status                          # list all programs + status
sudo supervisorctl start laravel-worker:*          # start all instances
sudo supervisorctl stop laravel-worker:*           # stop all
sudo supervisorctl restart laravel-worker:*        # graceful restart
sudo supervisorctl tail -f laravel-worker          # tail stdout
sudo supervisorctl tail -f laravel-worker stderr   # tail stderr

The * wildcard handles grouped processes — laravel-worker:* hits all 4 instances at once.

Group multiple programs

If your app has several different worker types, group them for atomic start/stop:

ini
# /etc/supervisor/conf.d/myapp-workers.conf

[program:email-worker]
command=php /home/myapp/artisan queue:work redis --queue=emails
user=myappuser
autostart=true
autorestart=true
stdout_logfile=/var/log/myapp/email-worker.log

[program:image-worker]
command=php /home/myapp/artisan queue:work redis --queue=images
user=myappuser
autostart=true
autorestart=true
numprocs=2
stdout_logfile=/var/log/myapp/image-worker.log

[program:payment-worker]
command=php /home/myapp/artisan queue:work redis --queue=payments --timeout=300
user=myappuser
autostart=true
autorestart=true
stopwaitsecs=300
stdout_logfile=/var/log/myapp/payment-worker.log

[group:myapp]
programs=email-worker,image-worker,payment-worker

Now you can manage the whole group:

bash
sudo supervisorctl restart myapp:*
sudo supervisorctl status myapp:*

Environment variables — secure handling

Two options, both with trade-offs.

Option 1: environment= directive

ini
[program:myapp-worker]
command=php worker.php
environment=
    DB_PASSWORD="secret123",
    APP_KEY="base64:...",
    REDIS_PASSWORD="another-secret"

Problem: values are visible to every user on the system via ps or /proc/PID/environ.

Option 2: source from env file (preferred)

ini
[program:myapp-worker]
command=/bin/bash -c "source /home/myapp/.env && exec php worker.php"
user=myappuser

Here the env file has restricted permissions:

bash
sudo chown myappuser:myappuser /home/myapp/.env
sudo chmod 600 /home/myapp/.env

Only myappuser can read the env file. ps doesn't show the expanded values — it shows the literal /bin/bash -c "source...".

Log rotation — built-in + logrotate

Supervisord has built-in log rotation:

ini
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5

At 10 MB, the log rotates to .log.1, keeps 5 archives (.log.1.log.5), deletes older. Total disk footprint capped at 60 MB.

For complementary rotation via the system's logrotate:

# /etc/logrotate.d/myapp
/var/log/myapp/*.log {
    daily
    rotate 14
    compress
    missingok
    notifempty
    copytruncate
}

copytruncate is important — it copies the log file and truncates the original in place, without needing to signal supervisord to reopen the file.

Running as a non-root user

Every program config should have user=. Running workers as root is a security risk — if the worker is compromised, the attacker has root on the VPS.

ini
user=myappuser

The user must exist:

bash
sudo adduser --system --group --no-create-home myappuser

File permissions: the worker user must be able to read the app code and write to log directories and any upload/storage dirs.

Supervisord vs systemd

Modern Linux has systemd, which also manages services. Overlap is significant. When to pick which:

Choose Supervisord if...Choose systemd if...
Multi-process grouping (numprocs=N) mattersSingle-process services
You want to manage many small workersYou want tight integration with OS services
You want supervisorctl controlYou prefer systemctl
Simpler config syntaxYou need socket activation, scheduled runs, etc.

For Laravel queue workers specifically, Supervisord is the canonical recommendation — the Laravel docs show Supervisord config, not systemd. For a single long-running Node service, systemd is fine.

Don't mix — pick one process manager per server and stick with it.

Common patterns

Laravel queue worker

Covered in detail above. Key points:

  • --max-time=3600 in the artisan command — worker self-exits every hour, Supervisord restarts it. Handles slow memory leaks.
  • numprocs=N for N parallel workers
  • Grace period stopwaitsecs=3600 so queue:restart doesn't kill mid-job

Python Celery worker

ini
[program:celery-worker]
command=/home/myapp/venv/bin/celery -A myapp worker --loglevel=info --concurrency=4
directory=/home/myapp
user=myappuser
autostart=true
autorestart=true
stdout_logfile=/var/log/myapp/celery-worker.log
stopwaitsecs=600

Celery has its own concurrency model — use --concurrency in Celery and numprocs=1 in Supervisord, or vice versa. Don't multiply both.

Node.js script (if not using PM2)

ini
[program:nodeapp]
command=/usr/bin/node /home/myapp/server.js
directory=/home/myapp
user=myappuser
autostart=true
autorestart=true
environment=NODE_ENV="production",PORT="3000"
stdout_logfile=/var/log/myapp/node.log

For Node.js specifically, PM2 offers better ergonomics (cluster mode, zero-downtime reload). Use PM2 for Node, Supervisord for everything else.

Long-running Python ETL script

ini
[program:etl]
command=/home/etl/venv/bin/python /home/etl/pipeline.py
directory=/home/etl
user=etl
autostart=true
autorestart=true
startsecs=30                    # must run 30s before considered "started successfully"
startretries=3                  # 3 fail attempts then stop trying
stopsignal=TERM
stopwaitsecs=60
stdout_logfile=/var/log/etl.log

startsecs prevents rapid-fail loops. If the process crashes within 30 seconds of starting, it's not counted as "started" — after startretries such crashes, supervisord marks it FATAL and stops.

Troubleshooting

Process keeps flapping (start → crash → start → crash)

bash
sudo supervisorctl status

If status is BACKOFF or FATAL, the program is crashing within startsecs. Read the log:

bash
sudo supervisorctl tail -f laravel-worker

Common root causes: permissions (file the worker needs is chmod 600 owned by someone else), missing env vars, syntax error in the code, dependency not installed in the venv.

Logs fill the disk

You disabled log rotation, or left the defaults too high. Revisit stdout_logfile_maxbytes and stdout_logfile_backups. df -h /var/log to see disk usage.

Config changes not picked up

supervisorctl update applies config changes but only picks up new/changed configs. For drastic reconfig, sudo systemctl restart supervisor.

Can't connect to supervisorctl socket

bash
sudo supervisorctl

Gives "unix:///var/run/supervisor.sock no such file". Usually means supervisor isn't running:

bash
sudo systemctl start supervisor

Processes run twice on config change

reread then update — don't forget both. reread finds the new config; update applies it. Skipping update leaves the new config unapplied while an old version runs.

Common pitfalls

  1. Running as root. Always set user=. Workers should have the minimum permissions they need.
  2. Shared log files across programs. Each program writes to its own log. Merging causes log interleaving.
  3. Forgetting `autorestart=true`. Crashes don't recover without it.
  4. `stopwaitsecs` too short. For jobs that take minutes (large batch, long API call), a 10-second stopwaitsecs kills mid-job on restart.
  5. Hardcoding paths. Use absolute paths everywhere — supervisord runs with a minimal environment, /usr/local/bin/node may not be on PATH.
  6. Missing `directory=`. Worker runs from / — relative paths break.
  7. Single log file for all `numprocs` workers. Log rotation gets confused. Use %(process_num)02d in the log file name for per-process logs.

Frequently asked questions

Can Supervisord run on shared hosting?

No. Requires root and long-lived processes. Only VPS.

How do I know if a job completed vs is hanging?

Supervisord doesn't track "job completion" — it tracks process liveness. For job-level observability, your queue system (Laravel Horizon, Bull Board) is the tool.

What's the difference between `stop` and `kill` in supervisorctl?

stop sends SIGTERM, waits stopwaitsecs, then SIGKILL if still running. kill sends SIGKILL immediately. Always prefer stop.

Can I reload config without restarting processes?

reread + update only restarts programs whose config changed. Unchanged programs keep running. For total config reload: supervisorctl reload (stops and restarts supervisord itself — all programs restart).

Should I use Supervisord or Docker Compose for multi-service apps?

If all services are containers: Docker Compose. If you're running bare processes on a VPS: Supervisord. You can also combine — Supervisord inside a container for multi-process containers (generally not recommended; prefer one process per container).

Can I monitor Supervisord remotely?

Yes — enable the HTTP interface in /etc/supervisor/supervisord.conf:

ini
[inet_http_server]
port=127.0.0.1:9001
username=admin
password=secret

Access via http://localhost:9001 (or over SSH tunnel). There's a web UI with basic controls.

Is Supervisord still actively maintained?

Yes. Stable releases continue. It's not rapidly adding features because there's not much more to add — the scope is narrow and well-defined.


Need help setting up Supervisord for your specific app? [email protected] — we help VPS customers with worker deployment as part of standard support.

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket