RoadRunner PHP Application Server on AWS User Guide
Overview
This image runs RoadRunner, a high-performance PHP application server and process manager written in Go. Instead of re-bootstrapping the PHP interpreter on every request the way PHP-FPM does, RoadRunner starts a pool of long-lived PHP worker processes once and keeps them warm, then dispatches each incoming request to a ready worker over an efficient pipes relay. The result is dramatically lower per-request overhead, predictable memory use, graceful worker recycling and built-in supervision. RoadRunner speaks PSR-7 and PSR-15 to your PHP application and adds HTTP, HTTP/2, gRPC, background jobs, queues and key-value plugins from a single static binary.
The image is delivered ready to run. RoadRunner v2025.1.15 is installed as the official prebuilt Go binary at /usr/local/bin/rr, PHP 8.3 and Composer are already installed, and a minimal demo PSR-7 worker application is vendored and running so you can prove the stack end to end within seconds of launch. RoadRunner is served to the network through an nginx reverse proxy on port 80, and its own HTTP, status and RPC listeners bind only to loopback.
This is a headless application-server runtime. RoadRunner has no web admin interface and no default password: the demo page is served content only and exposes no privileged surface, so there is nothing to rotate and nothing to leak. Administration is over SSH with your own key, plus the rr CLI and the loopback health endpoint.
Prerequisites
Before you deploy this image you need:
- An Amazon Web Services account where you can launch EC2 instances
- IAM permissions to launch instances, create security groups, and subscribe to AWS Marketplace products
- An EC2 key pair in the target Region for SSH access to the instance
- A VPC and subnet in the target Region, with a security group allowing inbound port 22 from your management network and port 80 from the clients that will reach the application
- The AWS CLI (version 2) installed locally if you plan to deploy from the command line
Step 1: Launch the Instance from the AWS Marketplace
Sign in to the AWS Management Console, open the EC2 service, and select Launch instance. Under Application and OS Images choose AWS Marketplace AMIs and search for RoadRunner. Select the cloudimg listing and choose Select, then Continue on the subscription summary.
Pick an instance type of m5.large or larger. Choose your EC2 key pair under Key pair (login). Under Network settings select your VPC and subnet, and either create or select a security group that opens port 22 from your management network and port 80 from the clients that will reach the application. Leave the root volume at the default size or larger.
Select Launch instance. First-boot initialisation takes a few seconds after the instance state becomes Running and the status checks pass; a first-boot service resolves the instance's public address and starts RoadRunner and nginx.
Step 2: Launch the Instance from the AWS CLI
The following block launches an instance from the cloudimg RoadRunner Marketplace AMI into an existing subnet and security group. Replace <ami-id> with the AMI ID shown on the Marketplace listing, <key-name> with your EC2 key pair name, <subnet-id> with your subnet ID, and <security-group-id> with a security group that opens ports 22 and 80.
aws ec2 run-instances \
--image-id <ami-id> \
--instance-type m5.large \
--key-name <key-name> \
--subnet-id <subnet-id> \
--security-group-ids <security-group-id> \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=roadrunner}]'
When the instance reaches the Running state and its status checks pass, note its public IP address or DNS name from the EC2 console or with aws ec2 describe-instances.
Step 3: Connect to Your Instance
Connect over SSH using your key pair and the login user for your operating system variant.
| OS variant | SSH login user |
|---|---|
| Ubuntu 24.04 | ubuntu |
ssh -i /path/to/your-key.pem ubuntu@<public-ip>
The login banner summarises where the demo application is served, how to inspect the worker pool, and where the per-instance info note lives.
Step 4: Open the Demo Application
RoadRunner is reachable over the network through nginx on port 80. Browse to the instance's public address, or curl it from the instance, to see the demo PSR-7 status page. The page is rendered by a warm PHP worker and reports the RoadRunner version, the PHP version, the worker PID serving the request, and how many requests that warm worker has served.
curl -s http://127.0.0.1/
The status fields render as:
RoadRunner version = 2025.1.15
PHP version = 8.3.6
Serving worker PID = 11022
Requests served by this warm worker = 4
Host = 127.0.0.1
Server time = 2026-07-22T14:09:19+00:00
To reach it from your browser, use http://<instance-public-ip>/.
Step 5: Confirm the Services and the Security Posture
RoadRunner and nginx run under systemd and start automatically on boot. Confirm both are active:
systemctl is-active roadrunner nginx
active
active
RoadRunner binds its HTTP (8080), status (2114) and RPC (6001) listeners to loopback only; nginx is the sole public listener on port 80. Confirm the binding:
sudo ss -tlnH | awk '{print $4}' | grep -E ':8080$|:2114$|:6001$|:80$' | sort
0.0.0.0:80
127.0.0.1:2114
127.0.0.1:6001
127.0.0.1:8080
[::]:80
The RoadRunner listeners are never exposed outside the instance. All external traffic arrives through nginx, which is ready for you to add your TLS certificate.
Step 6: Inspect the Warm Worker Pool
The core of RoadRunner is its pool of persistent PHP worker processes. Inspect it with the rr workers command against the shipped config:
sudo rr workers -c /etc/roadrunner/.rr.yaml
Workers of [http]:
+-------+--------+-------+--------+-------+----------------+
| PID | STATUS | EXECS | MEMORY | CPU % | CREATED |
+-------+--------+-------+--------+-------+----------------+
| 11022 | ready | 2 | 31 MB | 0.08 | 51 seconds ago |
| 11023 | ready | 2 | 31 MB | 0.08 | 51 seconds ago |
| 11024 | ready | 2 | 31 MB | 0.08 | 51 seconds ago |
| 11025 | ready | 1 | 31 MB | 0.08 | 51 seconds ago |
+-------+--------+-------+--------+-------+----------------+
Each worker is a warm PHP process that stays resident between requests. The EXECS column shows how many requests each worker has served. You can prove the workers stay warm by making several requests to the JSON health route and watching the per-worker counter climb:
for i in 1 2 3 4; do curl -s http://127.0.0.1/healthz; echo; done
{"status":"ok","worker_pid":11024,"php":"8.3.6","roadrunner":"2025.1.15","served_by_worker":4}
{"status":"ok","worker_pid":11023,"php":"8.3.6","roadrunner":"2025.1.15","served_by_worker":4}
{"status":"ok","worker_pid":11025,"php":"8.3.6","roadrunner":"2025.1.15","served_by_worker":4}
{"status":"ok","worker_pid":11022,"php":"8.3.6","roadrunner":"2025.1.15","served_by_worker":5}
The rising served_by_worker value confirms the interpreter is bootstrapped once per worker and kept warm, rather than re-bootstrapped per request.
Step 7: Health Checks
RoadRunner's built-in status plugin exposes a health endpoint on loopback for readiness probes. It returns HTTP 200 when the HTTP plugin's worker pool is healthy:
curl -s -o /dev/null -w 'rr status/health -> HTTP %{http_code}\n' 'http://127.0.0.1:2114/health?plugin=http'
rr status/health -> HTTP 200
The demo application also serves a JSON health route at /healthz through nginx, shown in Step 6, which you can point a load balancer target group or uptime monitor at.
Step 8: Replace the Demo Application with Your Own
The demo worker lives at /var/www/roadrunner-demo, with its Composer dependencies vendored under vendor/. To run your own application, either replace the demo worker script or point RoadRunner's server.command at your framework's RoadRunner worker.
The RoadRunner configuration is at /etc/roadrunner/.rr.yaml. The server.command line names the PHP worker RoadRunner runs, and the http.pool.num_workers line controls the pool size:
sudo cat /etc/roadrunner/.rr.yaml
Replace the worker script in place with your preferred editor (run as the roadrunner service account so file ownership stays correct):
sudo -u roadrunner nano /var/www/roadrunner-demo/worker.php
Then reload the worker pool without dropping the listener:
sudo rr reset -c /etc/roadrunner/.rr.yaml
If you change .rr.yaml itself (for example the pool size or the worker command), restart the service instead:
sudo systemctl restart roadrunner
Popular PHP frameworks including Symfony and Laravel ship first-class RoadRunner integration; point server.command at the framework's RoadRunner entrypoint, run composer install for your project, and reload.
Step 9: The Per-Instance Info Note
A root-only note at /root/roadrunner-info.txt records the resolved public URL and the key service hints for this instance. It contains no secret, because RoadRunner has none.
sudo cat /root/roadrunner-info.txt
ROADRUNNER_URL=http://<instance-public-ip>/
ROADRUNNER_HEALTH=http://127.0.0.1:2114/health?plugin=http
ROADRUNNER_DEMO_APP=/var/www/roadrunner-demo
ROADRUNNER_CONFIG=/etc/roadrunner/.rr.yaml
NOTE=Loopback-bound rr behind nginx :80. Reload workers with: rr reset -c /etc/roadrunner/.rr.yaml
Step 10: TLS and Production Hardening
nginx is the single public entry point and is ready for your TLS certificate. For production you should:
- Add a TLS certificate to the nginx virtual host, or terminate TLS at an Application Load Balancer in front of the instance and forward to port 80
- Restrict the security group so port 80 (or 443) is open only to the clients or load balancer that need it, and port 22 only to your management network
- Point an Application Load Balancer health check at
/healthz - Stream the nginx and RoadRunner journald logs to Amazon CloudWatch Logs for centralised monitoring
cloudimg support can help with connector configuration and AWS Certificate Manager integration.
Troubleshooting
- The demo page does not load over port 80 — confirm the security group allows inbound port 80 from your client, and that both services are active with
systemctl is-active roadrunner nginx. Check nginx withsudo nginx -tandjournalctl -u nginx. - RoadRunner will not start — inspect the journal with
journalctl -u roadrunner --no-pager | tail -50. A common cause is a syntax error in a replacedworker.php; runphp -l /var/www/roadrunner-demo/worker.phpto lint it. - Worker pool looks empty — run
sudo rr workers -c /etc/roadrunner/.rr.yaml. If no workers appear, restart the service withsudo systemctl restart roadrunnerand re-check. - Changes to the worker are not reflected — reload the pool with
sudo rr reset -c /etc/roadrunner/.rr.yaml; workers cache the compiled application, so a reset is required after editing the worker.
Support
This image is published and maintained by cloudimg with 24/7 support. For deployment help, TLS and load balancer configuration, worker-pool tuning, or onboarding your own PSR-7 application, contact the cloudimg support team at support@cloudimg.co.uk. RoadRunner is distributed under the MIT license.