Powered by
Migrating from PHP-FPM

Migrating from PHP-FPM

FrankenPHP replaces both your web server (Nginx, Apache) and PHP-FPM with a single binary. This guide covers a basic migration for a typical PHP application.

# Key Differences

PHP-FPM setupFrankenPHP equivalent
Nginx/Apache + PHP-FPMSingle frankenphp binary
php-fpm.conf pool settingsfrankenphp global option
Nginx server {} blockCaddyfile site block
php_value / php_admin_valuephp_ini Caddyfile directive
pm = static / pm.max_childrennum_threads
pm = dynamicmax_threads auto

# Step 1: Replace Your Web Server Config

A typical Nginx + PHP-FPM configuration:

server {
    listen 80;
    server_name example.com;
    root /var/www/app/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        fastcgi_pass unix:/run/php/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }
}

Becomes a single Caddyfile:

example.com {
    root /var/www/app/public
    php_server
}

That’s it. The php_server directive handles PHP routing, try_files-like behavior, and static file serving.

# Step 2: Migrate PHP Configuration

Your existing php.ini works as-is. See Configuration for where to place it depending on your installation method.

You can also set directives directly in the Caddyfile:

{
    frankenphp {
        php_ini memory_limit 256M
        php_ini max_execution_time 30
    }
}

example.com {
    root /var/www/app/public
    php_server
}

# Step 3: Adjust Pool Size

In PHP-FPM, you tune pm.max_children to control the number of worker processes. In FrankenPHP, the equivalent is num_threads:

{
    frankenphp {
        num_threads 16
    }
}

By default, FrankenPHP starts 2 threads per CPU. For dynamic scaling similar to PHP-FPM’s pm = dynamic:

{
    frankenphp {
        num_threads 4
        max_threads auto
    }
}

# Step 4: Docker Migration

A typical PHP-FPM Docker setup using two containers (Nginx + PHP-FPM) can be replaced by a single container:

Before:

services:
  nginx:
    image: nginx:1
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - .:/var/www/app
    ports:
      - "80:80"

  php:
    image: php:8.5-fpm
    volumes:
      - .:/var/www/app

After:

services:
  php:
    image: dunglas/frankenphp:1-php8.5
    volumes:
      - .:/app/public
      - caddy_data:/data
      - caddy_config:/config
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"

volumes:
  caddy_data:
  caddy_config:

If you need additional PHP extensions, see Building Custom Docker Image.

For framework-specific Docker setups, see Symfony Docker and Laravel.

# Step 5: Consider Worker Mode (Optional)

In classic mode, FrankenPHP works like PHP-FPM: each request boots the application from scratch. This is a safe starting point for migration.

For better performance, you can switch to worker mode, which boots your application once and keeps it in memory:

example.com {
    root /var/www/app/public
    php_server {
        root /var/www/app/public
        worker index.php 4
    }
}

Caution

Worker mode keeps your application in memory between requests. Make sure your code does not rely on global state being reset between requests. Frameworks like Symfony, Laravel, and API Platform have native support for this mode.

# What You Can Remove

After migrating, you no longer need:

  • Nginx or Apache
  • PHP-FPM (php-fpm service/process)
  • FastCGI configuration
  • Self-managed TLS certificates (Caddy handles them automatically)
Edit this page