Docker ComposeFrankenPHPPHP-FPM

Run PHP locally.

Two complete Docker Compose setups for your own machine: FrankenPHP as a single application server, or Nginx in front of a PHP-FPM worker pool.

Three ways to execute PHP

RuntimeRequest pathBest fit
Browser WebAssemblyEditor → Web Worker → PHP WASMExercises without a server installation, like the lessons on this site (PHP 8.5.10)
FrankenPHPHTTP → Caddy + embedded PHPOne application server with classic or worker mode
PHP-FPMHTTP → Nginx → FastCGI → PHP worker poolA 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 down

PHP-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 down

A 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.

Official reading