FrankenPHP on AWS User Guide
Overview
This image runs FrankenPHP, a modern PHP application server built on top of the Caddy web server. FrankenPHP embeds the PHP interpreter and Caddy into a single self contained binary, so you run high performance PHP without wiring together a separate web server and a PHP FPM pool. It adds worker mode for long running application workers, automatic HTTPS, early hints, and real time features such as server sent events and the Mercure hub, while remaining fully compatible with your existing PHP applications.
FrankenPHP is installed from the official prebuilt release binary and runs under systemd as an unprivileged frankenphp user with just the CAP_NET_BIND_SERVICE capability, so it can bind the standard privileged web ports without running as root. It serves your application over HTTPS on port 443 and permanently redirects plain HTTP on port 80 to HTTPS. A small demo PHP application ships in the document root so a freshly launched instance immediately demonstrates end to end PHP execution.
Secure by default. FrankenPHP embeds Caddy, whose administration interface on port 2019 can rewrite the running configuration over the network. This image disables that interface entirely (admin off), so it listens on no interface at all. On the first boot of every instance a one shot service generates a fresh per instance self signed TLS certificate bound to that instance address, so the server answers over HTTPS immediately with no shared or default certificate. FrankenPHP has no login, so no shared or default credential of any kind ships in the image.
What is included:
- FrankenPHP from the official prebuilt release binary, run under systemd as the unprivileged
frankenphpuser (frankenphp.service) - An HTTPS endpoint on port 443 and an HTTP to HTTPS permanent redirect on port 80
- A demo PHP application at
/var/www/html/index.phpthat reports the runtime PHP version, proving the application server executes PHP (replace it with your own application) - The embedded PHP interpreter and Caddy web server as a single static binary with no runtime library dependencies and no separate PHP FPM socket to manage
- The Caddy admin API disabled (
admin off) so the config mutation endpoint listens on no interface - A per instance self signed TLS certificate generated on first boot, with a one line path to automatic trusted HTTPS for your own domain
- The whole configuration in one readable file,
/etc/frankenphp/Caddyfile, that you keep under version control
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 inbound ports 443 and 80 from the networks your users will reach the application from
- The AWS CLI (version 2) installed locally if you plan to deploy from the command line
Recommended instance type: m5.large (2 vCPU, 8 GB RAM) is a sound starting point. Size up for production PHP workloads and worker mode concurrency; a demo or a small application runs comfortably on a smaller type.
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 FrankenPHP. 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 allows inbound port 22 from your management network and inbound ports 443 and 80 from the networks your users use. 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; the one shot service generates the per instance TLS certificate before the web server starts.
Step 2: Launch the Instance from the AWS CLI
The following block launches an instance from the cloudimg FrankenPHP 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, 443, and 80 as described above.
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> \
--block-device-mappings '[{"DeviceName":"/dev/sda1","Ebs":{"VolumeSize":20,"VolumeType":"gp3"}}]' \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=frankenphp-01}]'
The command prints a JSON document on success. Note the instance ID, then retrieve its public address once it is running with aws ec2 describe-instances --instance-ids <instance-id> --query "Reservations[].Instances[].PublicIpAddress" --output text.
Step 3: Connect to Your Instance
Connect over SSH with the key pair you selected and the public IP address from step 2. The SSH login user depends on the operating system of the AMI variant you launched:
| AMI variant | SSH login user |
|---|---|
| FrankenPHP on Ubuntu 24.04 | ubuntu |
ssh <login-user>@<public-ip>
The first boot service runs before the SSH daemon becomes ready, so the per instance certificate and the server information note are always in place when you log in for the first time.
Step 4: Review the Server Information Note
FrankenPHP has no login and no password, so the image ships no credential of any kind. A short, non secret information note is written on first boot, recording the endpoint URLs, the certificate path, and where the configuration and document root live. Read it with:
sudo cat /root/frankenphp-info.txt
The note is unique to your instance. HTTPS_URL points at your instance address, and the certificate the server presents is the per instance self signed certificate generated on first boot.
Step 5: Confirm the Service and PHP Execution
Confirm the systemd service is active:
sudo systemctl --no-pager status frankenphp
● frankenphp.service - FrankenPHP - modern PHP application server (Caddyfile-driven)
Loaded: loaded (/etc/systemd/system/frankenphp.service; enabled; preset: enabled)
Active: active (running)
Main PID: 2521 (frankenphp)
CGroup: /system.slice/frankenphp.service
└─2521 /opt/frankenphp/frankenphp run --config /etc/frankenphp/Caddyfile
Check the installed FrankenPHP, PHP and Caddy versions:
sudo -u frankenphp HOME=/var/lib/frankenphp /opt/frankenphp/frankenphp version
FrankenPHP v1.12.4 PHP 8.5.8 Caddy v2.11.4
Fetch the demo page over HTTPS from the instance itself. The -k flag accepts the per instance self signed certificate. The page is rendered by PHP and reports the runtime PHP version, which proves the application server executed the code rather than returning a static file:
curl -ksS https://127.0.0.1/ | grep -E 'rendered by PHP|execution check'
<p class="ok">This page was rendered by PHP 8.5.8 — the FrankenPHP application server is running.</p>
<tr><th>PHP execution check (6 x 7)</th><td>42</td></tr>
The PHP execution check (6 x 7) cell reads 42: the value was computed by PHP at request time, so if you can see it, PHP is executing.
Confirm plain HTTP is permanently redirected to HTTPS:
curl -ksSI http://127.0.0.1/ | grep -E 'HTTP/|Location:'
HTTP/1.1 301 Moved Permanently
Location: https://127.0.0.1/
Step 6: Browse to the Demo Application
From your workstation, open https://<public-ip>/ in a browser. Because the certificate is a per instance self signed certificate, your browser shows a certificate warning the first time; accept it to continue (for a trusted certificate on your own domain, see step 9). The demo landing page reports the PHP version it was rendered by, confirming end to end PHP execution over HTTPS.

Step 7: Review the Configuration
The entire behaviour of the server is defined by one readable file, the Caddyfile:
sudo cat /etc/frankenphp/Caddyfile
The global options block sets admin off (the Caddy admin API is disabled and listens on no interface), enables the embedded frankenphp application server, and manages the HTTP to HTTPS redirect. The :443 site block terminates TLS with the per instance certificate and serves /var/www/html through the php_server directive.
You can confirm for yourself that the admin API is not listening on any interface:
ss -ltnH 'sport = :2019' | wc -l
0
A result of 0 means nothing is listening on port 2019.
Step 8: Deploy Your Own PHP Application
Copy your PHP application into the document root at /var/www/html, then restart the service. Because the admin API is disabled, configuration changes are applied by a brief service restart rather than over a network endpoint:
sudo rsync -a ./my-app/ /var/www/html/
sudo chown -R frankenphp:frankenphp /var/www/html
sudo systemctl restart frankenphp
If your application uses a front controller (for example Laravel or Symfony, whose entry point is public/index.php), point the Caddyfile root at that directory instead. Edit /etc/frankenphp/Caddyfile, change the root * /var/www/html line inside the :443 block to your application's public directory, and restart the service.
Step 9: Use a Trusted HTTPS Certificate for Your Own Domain
The image ships with a per instance self signed certificate so it works immediately with no domain. If you own a domain and point its DNS at the instance, FrankenPHP can obtain a trusted certificate automatically. Edit /etc/frankenphp/Caddyfile, replace the whole :443 { ... } block with a block named for your domain, and restart the service:
your-domain.com {
root * /var/www/html
encode gzip
php_server
}
On the next request Caddy provisions a Let's Encrypt certificate for your-domain.com automatically and renews it for you. For this to work, ports 80 and 443 must be reachable from the internet and the domain's DNS A record must resolve to the instance public IP.
Step 10: Enable Worker Mode (Optional)
Worker mode keeps your application booted in memory between requests for a large throughput gain on frameworks that support it. Consult your framework's FrankenPHP worker documentation for the worker script, then reference it in the Caddyfile frankenphp block. Restart the service after any change with sudo systemctl restart frankenphp and watch the logs with sudo journalctl -u frankenphp -f.
Backup and Maintenance
- Configuration and content live in
/etc/frankenphp/Caddyfileand/var/www/html. Keep the Caddyfile under version control and snapshot the root EBS volume to back up your application. - Logs are available with
sudo journalctl -u frankenphp. FrankenPHP logs atERRORlevel by default; raise the level in the Caddyfilelogblock while debugging. - Updates. To move to a newer FrankenPHP release, replace the binary at
/opt/frankenphp/frankenphpwith the new official static binary and restart the service. cloudimg publishes refreshed images as new versions are released. - Certificates. The per instance self signed certificate is valid for ten years. If you switch to a trusted certificate for your own domain, Caddy renews it automatically.
Security Notes
- The Caddy admin API is disabled, so there is no network configuration surface to protect.
- No application credential ships in the image; FrankenPHP has no login.
- Restrict SSH (port 22) to your management network in the security group, and open ports 443 and 80 only to the networks that need to reach your application.
- The service runs as the unprivileged
frankenphpuser with onlyCAP_NET_BIND_SERVICE, under systemd hardening (NoNewPrivileges,ProtectSystem,ProtectHome,PrivateTmp).
Support
cloudimg provides 24/7 technical support for this FrankenPHP image by email at support@cloudimg.co.uk and live chat. We help with deployment and first boot configuration, editing the Caddyfile, deploying your PHP application, enabling and tuning worker mode, configuring HTTPS with per instance self signed certificates or trusted certificates for your own domain, wiring up real time features, performance tuning, version upgrades, and troubleshooting. Critical issues receive a one hour average response time.