francodacosta/supervisord
PHP client for Supervisord's XML-RPC API that lets you start, stop, restart and inspect supervisor programs and processes, read logs, and check status from Laravel or any plain PHP application.
This is a small, framework-agnostic PHP library whose only job is to render supervisord .ini configuration files. It ships no service provider, no facade, and no Laravel integration — you composer require francodacosta/supervisord and call it directly, typically from an Artisan command or deploy script.
Because the package is thin (3 stars, no dependents) and largely undocumented, the first thing to do is read vendor/francodacosta/supervisord/src — it is a handful of classes modelling the supervisord sections (Supervisord / Program / ProgramGroup). Names in the snippets below follow that structure; confirm them for the pinned version.
The most useful first use case is turning a hand-maintained /etc/supervisor/conf.d/laravel.conf into generated output:
use Francodacosta\Supervisord\Supervisord;
use Francodacosta\Supervisord\Program;
$config = new Supervisord();
$worker = new Program();
$worker->setName('laravel-queue-default');
$worker->setCommand('/usr/bin/php /var/www/app/artisan queue:work --queue=default --sleep=1 --tries=3');
$worker->setNumprocs(4);
$worker->setProcessName('%(program_name)s_%(process_num)02d');
$worker->setUser('www-data');
$worker->setAutostart(true);
$worker->setAutorestart(true);
$config->addProgram($worker);
file_put_contents('/etc/supervisor/conf.d/laravel.conf', $config->getConfig());
Then supervisorctl reread && supervisorctl update applies it.
Config-driven generation. Keep a config/supervisor.php that describes programs declaratively, and let one Artisan command render everything. This is where the package earns its keep — it saves you from string-concatenating INI.
// config/supervisor.php
return [
'programs' => [
'laravel-queue-default' => [
'command' => 'php artisan queue:work --queue=default --tries=3 --sleep=1',
'numprocs' => (int) env('QUEUE_PROCS', 4),
'user' => 'www-data',
],
'laravel-scheduler' => [
'command' => 'php artisan schedule:work',
'numprocs' => 1,
'user' => 'www-data',
],
],
];
class GenerateSupervisorConfig extends Command
{
protected $signature = 'supervisor:generate {--dry-run} {--path=/etc/supervisor/conf.d/laravel.conf}';
public function handle(): int
{
$config = new Supervisord();
foreach (config('supervisor.programs') as $name => $opts) {
$program = new Program();
$program->setName($name);
$program->setCommand(
str_replace('php artisan', PHP_BINARY.' '.base_path('artisan'), $opts['command'])
);
$program->setDirectory(base_path());
$program->setNumprocs($opts['numprocs']);
$program->setProcessName('%(program_name)s_%(process_num)02d');
$program->setUser($opts['user'] ?? 'www-data');
$program->setAutostart(true);
$program->setAutorestart(true);
$program->setStopwaitsecs(30);
$config->addProgram($program);
}
$ini = $config->getConfig();
if ($this->option('dry-run')) {
$this->line($ini);
return self::SUCCESS;
}
file_put_contents($this->option('path'), $ini);
$this->call('shell', ['cmd' => 'supervisorctl reread && supervisorctl update']);
return self::SUCCESS;
}
}
Deploy hook. Run php artisan supervisor:generate in your deploy pipeline after the new release directory exists and before the queue restart. Diff the generated file against the deployed one and only reread/update when it changed — unnecessary reloads bounce healthy workers.
Program groups. Use one group per app ([group:laravel]) so operators can supervisorctl restart laravel:* without touching other tenants on a shared box. This is the main reason to model sections instead of writing a heredoc.
Docker entrypoints. Generate at container boot from env vars (queue names, proc counts), then exec supervisord -n. This keeps images identical across environments.
Golden-file tests. Snapshot $config->getConfig() into a fixture and assert against it in CI. It catches accidental numprocs or command changes far earlier than a production queue that silently stops draining.
Coexistence with Horizon. Don't duplicate Horizon's own supervisor setup. Use this package only for non-Horizon daemons: schedule:work, queue:work --queue=low, or bespoke long-running workers (websocket consumers, feed pollers).
Thin wrapper. Wrap the library in your own SupervisorConfigBuilder so swapping to a Blade/heredoc template later is a one-file change — the package is small enough that this costs almost nothing and protects you from abandonment.
%(program_name)s, %(process_num)02d, and %(ENV_X)s are interpreted by supervisord. Any literal % in a command (URL-encoded args, date +%s) must be escaped as %%. Verify the library escapes for you or do it yourself.true/false in the INI; PHP setAutostart(true) is fine, but passing 'True' or 1 produces config supervisord rejects.numprocs > 1 requires process_name, otherwise every child gets the same name and supervisorctl cannot address them individually.PATH or cwd. Use PHP_BINARY/base_path('artisan') and set directory= explicitly — a relative php artisan will fail with can't find command..env from your project root for its own process, and child env comes from the config environment= line or the supervisor daemon's environment. If your workers depend on exported vars, set them explicitly.0644, owned by root, into a directory matched by an include in the main supervisord.conf. Missing include glob is the #1 "my generated file does nothing" cause.reread ≠ update. reread reloads file contents; update actually adds/removes/restarts programs. Changing a command requires both, and even then existing processes restart only with update.stdout_logfile/stderr_logfile (or /dev/stdout under Docker) and plan rotation — stdout_logfile_maxbytes. Unrotated queue worker logs fill disks quietly.stopsignal=TERM and a sane stopwaitsecs (e.g. 30) so in-flight jobs finish; killasgroup=true prevents orphaned child processes when using --tries wrappers.supervisord -n -c /path/to/supervisord.conf in the foreground, then supervisorctl status, supervisorctl tail -f <name> stderr.Process/shell_exec call — wrap and escape carefully, since program names go straight into a shell command.composer.json, and because the blast radius is "renders a text file," the wrapper approach above lets you drop it in an afternoon if needed.How can I help you explore Laravel packages today?