FrankenPHP
Overview
FrankenPHP is a modern PHP application server built on top of the Caddy web server. It 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.
FrankenPHP adds worker mode for long-running application workers, automatic HTTPS, early hints, and real-time features such as server-sent events and Mercure. It remains fully compatible with your existing PHP applications.
This guide covers deploying and configuring FrankenPHP on Ubuntu 24.04 on Azure using SecureAnyCloud Azure Marketplace images. The image installs FrankenPHP 1.12.4 from the official prebuilt release binary. It 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.
FrankenPHP 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. This image disables that interface entirely (admin off), so it listens on no interface at all.
On the first boot of every VM, 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. FrankenPHP has no login, so no shared or default credential of any kind ships in the image.
What you get:
- FrankenPHP 1.12.4 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 can keep under version control
Prerequisites
- An active Azure subscription, an SSH public key, and a VNet with a subnet in the target region
- A subscription to the FrankenPHP listing on Azure Marketplace
- Network Security Group rules that allow TCP 22 (admin) from your management network, and TCP 443 (HTTPS) plus TCP 80 (HTTP redirect) from the clients that reach your application
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is ample for a demo or a small application. Choose a larger size for production PHP workloads and worker mode concurrency.
Deploy from the Azure Portal
- Open the FrankenPHP offer on the Azure Marketplace and select Get It Now, then Create.
- Choose your subscription, resource group and region.
- Set the VM size (
Standard_B2sor larger), your SSH public key and the admin usernameazureuser. - On the Networking tab, attach a Network Security Group that allows inbound TCP 22 from your management network and TCP 443 and TCP 80 from your users.
- Review and create. When the deployment completes, note the VM public IP address.
Deploy from the Azure CLI
az vm create \
--resource-group my-rg \
--name my-frankenphp \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
# open HTTPS (443), the HTTP redirect (80) and SSH (22)
az vm open-port --resource-group my-rg --name my-frankenphp --port 443 --priority 1001
az vm open-port --resource-group my-rg --name my-frankenphp --port 80 --priority 1002First boot
On the first boot, the frankenphp-firstboot.service one-shot unit generates a fresh per-instance self-signed TLS certificate (bound to this VM's IP address). It writes an informational note to /root/frankenphp-info.txt and then starts frankenphp.service. This takes a few seconds.
SSH in as azureuser:
ssh azureuser@<vm-public-ip>Confirm the service is running
Confirm that frankenphp.service is active, check the FrankenPHP, PHP and Caddy versions, and confirm that the server is listening on port 443 (HTTPS) and port 80 (the redirect):
sudo systemctl is-active frankenphp HOME=/var/lib/frankenphp /opt/frankenphp/frankenphp version | head -1 sudo ss -tlnp | grep -E ':443 |:80 'The output shows active, a version line reporting FrankenPHP v1.12.4 PHP 8.5.8 Caddy v2.11.4, and the server listening on *:443 and *:80.
Verify PHP execution over HTTPS
PHP processes the demo application at /var/www/html/index.php. The served page reports the exact runtime PHP version and a computed value, so you can verify that the FrankenPHP application server is executing PHP rather than serving a static file. Fetch it over HTTPS. The -k flag accepts the per-instance self-signed certificate:
curl -ks https://127.0.0.1/ | grep -iE 'rendered by PHP|execution check'The response reports that the page was rendered by PHP 8.5.8 and shows the PHP execution check (6 x 7) evaluating to 42 on the server.
Open https://<vm-public-ip>/ in a browser. Your browser warns about the self-signed certificate until you switch to a trusted certificate in Step 8. The demo landing page confirms the runtime PHP version and provides quickstart notes.
HTTP to HTTPS redirect and the disabled admin API
Plain HTTP on port 80 permanently redirects (301) to HTTPS. Confirm the redirect, then confirm that the Caddy admin API on port 2019 is not listening on any interface (secure by default):
curl -ksI http://127.0.0.1/ | grep -iE 'HTTP/|location'
sudo ss -tln | grep ':2019' || echo 'admin API disabled - nothing listening on :2019'The output shows HTTP/1.1 301 Moved Permanently with a Location: header pointing at the HTTPS URL, and confirmation that nothing is listening on port 2019.
Deploy your own PHP application
The document root is /var/www/html. Copy your PHP application there (or point the Caddyfile root at another directory), then restart the service. Because the admin API is disabled, you apply config and code changes with a brief restart:
# copy your application into the document root
sudo rsync -a ./my-php-app/ /var/www/html/
# apply the change
sudo systemctl restart frankenphpThe php_server directive serves index.php as the front controller, so modern PHP frameworks such as Laravel and Symfony work out of the box once their public directory is the document root.
Enable trusted automatic HTTPS for your own domain
FrankenPHP inherits Caddy's automatic HTTPS. If you own a domain and point its DNS A record at this instance, you can switch from the per-instance self-signed certificate to a trusted, automatically renewed certificate with a single Caddyfile edit. Replace the :443 block in /etc/frankenphp/Caddyfile with your domain name:
your-domain.com { root * /var/www/html php_server }Then restart the service. On the first request to your domain, FrankenPHP provisions a certificate automatically and renews it before expiry. There is no Certbot dependency:
sudo systemctl restart frankenphpReview the configuration and info note
The whole server configuration is one readable file, and the first-boot note records this instance's endpoint details:
sudo cat /etc/frankenphp/Caddyfile
sudo cat /root/frankenphp-info.txtThe Caddyfile shows the admin off global option, the :80 redirect block and the :443 block serving php_server from /var/www/html.
The info note records the HTTPS and HTTP URLs for this instance and confirms that FrankenPHP has no login and no shared credential.
Security recommendations
- Restrict the management port. Allow inbound TCP 22 only from your management network, never
0.0.0.0/0. - Serve your users over HTTPS only. Expose TCP 443 to your users, and expose TCP 80 only so that the redirect works. Switch to a trusted certificate for your own domain (Step 8) so that browsers do not warn.
- Keep the admin API disabled. This image ships with the Caddy admin API disabled (
admin off). Leave it disabled unless you have a specific need. If you enable it, bind it to loopback only, never to a network interface. - Keep the OS patched. The image is patched at build time and ships with
unattended-upgradesenabled, so security updates continue automatically. Reboot after kernel updates. - Run your application with least privilege.
frankenphp.servicealready runs as the unprivilegedfrankenphpuser with a minimal capability set and systemd sandboxing.
Support and licensing
FrankenPHP is open source software distributed under the MIT License. This SecureAnyCloud image bundles FrankenPHP 1.12.4 with its embedded PHP 8.5 interpreter and the Caddy 2 web server on Ubuntu 24.04 LTS, patched at build time and hardened.
SecureAnyCloud provides the packaging, configuration, security hardening and support. It does not modify the upstream FrankenPHP licence.
Deploy on Azure
Launch FrankenPHP on Ubuntu 24.04 from the SecureAnyCloud Azure Marketplace listing and follow this guide to deploy your PHP application.
Need Help?
SecureAnyCloud provides 24/7 technical support for this FrankenPHP image. Contact sales@secureanycloud.com for help with worker mode tuning, Caddyfile configuration, HTTPS and certificate options, and deploying PHP frameworks.
All product and company names are trademarks or registered trademarks of their respective holders. Their use does not imply any affiliation with or endorsement by them.
Deploy on Azure
Launch FrankenPHP 1.12.4 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
FrankenPHP is a modern PHP application server built on top of the Caddy web server. It 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.
FrankenPHP 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. It remains fully compatible with your existing PHP applications.
This image installs FrankenPHP from the official prebuilt release binary. It 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. 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 you get:
- 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 can 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 that allows inbound port 22 from your management network and inbound ports 443 and 80 from the networks your users 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. Choose a larger type for production PHP workloads and worker mode concurrency. A demo or a small application runs comfortably on a smaller type.
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 SecureAnyCloud 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. 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.
Launch the instance from the AWS CLI
The following block launches an instance from the SecureAnyCloud FrankenPHP Marketplace AMI into an existing subnet and security group.
Replace <ami-id> with the AMI ID shown on the Marketplace listing and <key-name> with your EC2 key pair name. Replace <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 the instance's public address once it is running with aws ec2 describe-instances --instance-ids <instance-id> --query "Reservations[].Instances[].PublicIpAddress" --output text.
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. For FrankenPHP on Ubuntu 24.04, the user is ubuntu.
Run this command with your login user and the instance's public IP address in place of the angle-bracket values:
ssh <login-user>@<public-ip>The first-boot service runs before the SSH daemon becomes ready. The per-instance certificate and the server information note are therefore always in place when you log in for the first time.
Review the server information note
FrankenPHP has no login and no password, so the image ships no credential of any kind. On first boot, the image writes a short, non-secret information note. It records the endpoint URLs, the certificate path, and where the configuration and document root live. Read it with:
sudo cat /root/frankenphp-info.txtThe 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.
Confirm the service and PHP execution
Confirm that 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/CaddyfileCheck the installed FrankenPHP, PHP and Caddy versions:
sudo -u frankenphp HOME=/var/lib/frankenphp /opt/frankenphp/frankenphp versionFrankenPHP v1.12.4 PHP 8.5.8 Caddy v2.11.4Fetch 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 that 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. PHP computed the value at request time, so if you can see it, PHP is executing.
Confirm that 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/Browse to the demo application
From your workstation, open https://<public-ip>/ in a browser. The certificate is a per-instance self-signed certificate, so 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.
Review the configuration
One readable file, the Caddyfile, defines the entire behaviour of the server:
sudo cat /etc/frankenphp/CaddyfileThe 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 -l0A result of 0 means that nothing is listening on port 2019.
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, you apply configuration changes with 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 frankenphpIf 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 and change the root * /var/www/html line inside the :443 block to your application's public directory. Then restart the service.
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.
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/frankenphp with the new official static binary and restart the service. SecureAnyCloud 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, because 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
SecureAnyCloud provides 24/7 technical support for this FrankenPHP image by email at sales@secureanycloud.com 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.
Deploy on AWS
Launch FrankenPHP on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com