Three ways to execute PHP
| Runtime | Request path | Best fit |
|---|---|---|
| Browser WebAssembly | Editor → Web Worker → PHP WASM | Exercises without a server installation, like the lessons on this site (PHP 8.5.10) |
| FrankenPHP | HTTP → Caddy + embedded PHP | One application server with classic or worker mode |
| PHP-FPM | HTTP → Nginx → FastCGI → PHP worker pool | A separate web server and PHP process manager |
FrankenPHP: start with classic mode
FrankenPHP embeds PHP in a Caddy-based application server. Classic mode runs ordinary PHP request scripts. Worker mode keeps an application booted across requests and requires a compatible request loop; globals, services and memory need careful cleanup. The example below uses classic mode. The FrankenPHP guide covers worker mode.
Create a directory with these files, plus public/index.php from the shared example below.
compose.yaml
services:
app:
image: dunglas/frankenphp:1-php8.4-bookworm
ports:
- "127.0.0.1:8084:80"
volumes:
- ./public:/app/public:ro
- ./Caddyfile:/etc/frankenphp/Caddyfile:ro
Caddyfile
{
auto_https off
admin off
}
:80 {
root * /app/public
php_server
}
docker compose up -d
curl http://127.0.0.1:8084/
docker compose downPHP-FPM: Nginx and a PHP worker pool
FPM manages PHP processes and speaks FastCGI, not HTTP. Nginx receives the browser request and forwards PHP scripts to the internal php:9000 service. Both containers use the same script path. Only Nginx has a host port. See the PHP-FPM guide for pool settings and deployment.
compose.yaml
services:
php:
image: php:8.4-fpm-bookworm
volumes:
- ./public:/var/www/html:ro
web:
image: nginx:stable-alpine
ports:
- "127.0.0.1:8085:80"
volumes:
- ./public:/var/www/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
nginx.conf
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass php:9000;
}
}
docker compose up -d
curl http://127.0.0.1:8085/
docker compose downA shared PHP entry point
Save this file as public/index.php in either example directory. The response reports the actual PHP patch and server API, so you can see which runtime executed the script.
<?php
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['message' => 'Hello from PHP', 'version' => PHP_VERSION, 'sapi' => PHP_SAPI], JSON_THROW_ON_ERROR);
These are local development examples, bound to loopback with read-only source mounts. Their image tags track PHP 8.4 updates. For a production deployment, build your application image with its dependencies and extensions, pin a tested image digest, configure HTTPS and secrets, and add monitoring. A persistent FrankenPHP worker also needs framework support and state-reset tests.