spiral/roadrunner
RoadRunner is a high-performance PHP application server and process manager written in Go. Runs long-lived PHP workers and replaces Nginx+FPM setups. Extensible via plugins (HTTP/2/3, HTTPS, FastCGI), PSR-7/17 compatible, service-friendly.
## Getting Started
### Minimal Setup
1. **Installation**:
```bash
composer require spiral/roadrunner-cli
./vendor/bin/rr get-binary
Ensure PHP extensions php-curl, php-zip, and php-sockets are enabled.
Configure .rr.yaml:
version: '3'
server:
command: "php worker.php"
http:
address: "0.0.0.0:8080"
Create a Worker (worker.php):
use Spiral\RoadRunner;
use Nyholm\Psr7;
$worker = RoadRunner\Worker::create();
$psrFactory = new Psr7\Factory\Psr17Factory();
$httpWorker = new RoadRunner\Http\PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);
while ($req = $httpWorker->waitRequest()) {
$rsp = new Psr7\Response();
$rsp->getBody()->write('Hello RoadRunner!');
$httpWorker->respond($rsp);
}
Run:
./rr serve -c .rr.yaml
Replace your existing Nginx+PHP-FPM setup with RoadRunner’s HTTP server. Configure Nginx to proxy requests to 127.0.0.1:8080 (or your .rr.yaml port). Test with:
curl http://localhost:8080
App\Http\Middleware) by wrapping them in PSR-7 adapters.
use Spiral\RoadRunner\Http\PSR7Worker;
use Nyholm\Psr7\Factory\Psr17Factory;
$worker = RoadRunner\Worker::create();
$psrFactory = new Psr17Factory();
$httpWorker = new PSR7Worker($worker, $psrFactory, $psrFactory, $psrFactory);
while ($req = $httpWorker->waitRequest()) {
$response = app()->handle($req); // Laravel's PSR-7 handler
$httpWorker->respond($response);
}
static middleware in .rr.yaml:
http:
address: "0.0.0.0:8080"
middleware: ["static"]
jobs:
pool:
num_workers: 4
max_jobs: 0
supervisor: true
use Spiral\RoadRunner\Jobs\JobsWorker;
$jobsWorker = new JobsWorker($worker);
while ($job = $jobsWorker->wait()) {
$job->perform(); // Laravel's job handler
}
.proto files and mount them in .rr.yaml:
grpc:
proto: "proto/*.proto"
listen: "tcp://0.0.0.0:50051"
Generate PHP stubs with protoc and implement handlers in Laravel..rr.yaml:
otel:
endpoint: "http://localhost:4317"
service_name: "laravel-app"
Use Laravel’s spatie/laravel-observability package for integration.SIGTERM in your worker:
declare(ticks=1);
pcntl_signal(SIGTERM, function() {
$worker->getWorker()->stop();
});
YAML Syntax:
"tcp://127.0.0.1:6001").http:
address: "0.0.0.0:8080"
middleware: ["static", "headers"]
Plugin Conflicts:
fileserver and static middleware in the same HTTP config.jobs and http workers don’t share the same pool unless explicitly configured.TLS Management:
.rr.yaml:
http:
address: "0.0.0.0:443"
tls:
cert: "/path/to/cert.pem"
key: "/path/to/key.pem"
Logs:
logs.level: debug in .rr.yaml for verbose output.stderr or via rr logs.Worker Crashes:
supervisor: true in job/worker pools to auto-restart failed workers.rr logs --worker.Port Conflicts:
.rr.yaml (e.g., 8080, 6001) aren’t occupied by other services. Use lsof -i :8080 to check.Worker Pool Tuning:
num_workers in job/worker pools based on CPU cores (e.g., num_workers: 4 for 4 cores).max_jobs: 0 for unlimited concurrency (default) or limit with max_jobs: 100.HTTP Middleware:
gzip) if not needed to reduce overhead.headers before static to avoid double-processing.gRPC Optimization:
protobuf PHP extension for faster serialization:
pecl install protobuf
.rr.yaml:
grpc:
pool:
max_connections: 100
Custom Plugins:
.rr.yaml:
plugins:
my_plugin:
config: "path/to/config.json"
Laravel Service Providers:
$this->app->singleton(RoadRunner\Worker::class, function() {
return RoadRunner\Worker::create();
});
Dynamic Configuration:
SIGUSR2 to reload .rr.yaml without restarting:
kill -USR2 <rr_pid>
.rr.yaml to a target file (e.g., ln -s config/rr.yaml .rr.yaml).EOF Errors:
php-sockets extension or misconfigured worker command.php --modules | grep sockets and ensure server.command in .rr.yaml matches your worker script.Queue Stuck Jobs:
try-catch and log errors:
try {
$job->perform();
} catch (\Throwable $e) {
$worker->getWorker()->error($e->getMessage());
throw $e; // Requeue or mark as failed
}
Temporal Workflows:
NO_PROXY is set if using Temporal behind a proxy:
temporal:
env:
NO_PROXY: "localhost,127.0.0.1"
Docker Gotchas:
.rr.yaml and ensure volumes are writable.user: "www-data" in Dockerfile if running as non-root.rr watch to auto-reload workers on file changes (experimental).metrics:
address: "0.0.0.0:2112"
Scrape with http://localhost:2112/metrics.How can I help you explore Laravel packages today?