Skip to content
Nginx Php Fpm

Migrating from Nginx + PHP-FPM

You’re running the modern PHP stack — Nginx as a reverse proxy, PHP-FPM handling PHP execution via FastCGI. It’s fast and battle-tested, but it’s two services to configure, monitor, and scale independently.

ePHPm replaces both Nginx and PHP-FPM with a single binary. No FastCGI socket. No upstream configuration. No separate process manager.

What You’re Replacing

ComponentNginx + PHP-FPMePHPm
HTTP serverNginxBuilt-in (hyper)
PHP runtimePHP-FPM (separate process)Embedded via FFI (same process)
PHP ↔ HTTP communicationFastCGI over Unix socketIn-process function call
Process managementpm.dynamic / pm.staticWorker thread pool (php.workers)
Static filesNginx serves directlyBuilt-in with compression
TLS terminationNginx + certbotBuilt-in ACME
Services to manage2 (nginx + php-fpm)1

Step-by-Step Migration

1. Translate Nginx Config

Typical Nginx config for a PHP site:

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

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

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

    location ~ /\.ht {
        deny all;
    }

    location ~* \.(css|js|gif|ico|jpeg|jpg|png|svg|woff2?)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

ePHPm equivalent:

[server]
listen = "0.0.0.0:8080"
document_root = "/var/www/html"
index_files = ["index.php", "index.html"]
fallback = ["$uri", "$uri/", "/index.php?$query_string"]

[server.static]
cache_control = "public, max-age=2592000"

That’s the entire Nginx server block in 6 lines. No fastcgi_pass, no location blocks, no fastcgi_params.

2. Translate PHP-FPM Config

Typical www.conf pool config:

[www]
user = www-data
group = www-data
listen = /run/php/php8.2-fpm.sock
pm = dynamic
pm.max_children = 50
pm.start_servers = 5
pm.min_spare_servers = 5
pm.max_spare_servers = 35
pm.max_requests = 500

php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 30
php_admin_value[upload_max_filesize] = 64M

ePHPm equivalent:

[php]
workers = 8            # replaces pm.max_children (auto-detected from CPU count)
memory_limit = "256M"
max_execution_time = 30

[server.request]
max_body_size = 67108864   # 64 MB

PHP-FPM’s process model (pm.dynamic, pm.start_servers, spare servers) doesn’t apply — ePHPm uses a fixed-size thread pool that’s always warm. No cold starts, no process spawning overhead.

Connection and rate limits (Nginx’s worker_connections, limit_conn, limit_req):

worker_connections 1024;
limit_conn_zone $binary_remote_addr zone=addr:10m;
limit_conn addr 100;
limit_req_zone $binary_remote_addr zone=req:10m rate=10r/s;
limit_req zone=req burst=50;
[server.limits]
max_connections = 1024
per_ip_max_connections = 100
per_ip_rate = 10.0       # requests/sec per IP
per_ip_burst = 50

All limits default to 0 (unlimited). Set them for production to prevent abuse.

3. Common Nginx Location Blocks

Block dotfiles:

location ~ /\. { deny all; }

ePHPm: default behavior. Dotfiles are blocked automatically.

Open file cache (Nginx’s open_file_cache):

open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
[server.file_cache]
enabled = true
max_entries = 10000
valid_secs = 30         # re-stat interval
inactive_secs = 60      # evict entries not accessed within this window
inline_threshold = 1048576  # cache content for files under 1 MB
precompress = true       # pre-compute gzip for cached compressible files

When enabled, ePHPm caches file metadata (size, mtime, MIME type, ETag) and small file content in memory. Files above inline_threshold are streamed from disk in 64 KiB chunks instead of being read entirely into memory. This matches Nginx’s behavior of caching file descriptors and metadata for frequently accessed files.

Block vendor directory:

location ^~ /vendor/ { deny all; }
[server.security]
blocked_paths = ["vendor/*"]

PHP execution restriction:

location ~* /uploads/.*\.php$ { deny all; }
[server.security]
allowed_php_paths = ["/index.php", "/wp-admin/*", "/wp-login.php"]

Gzip compression:

gzip on;
gzip_types text/css application/javascript text/plain;
gzip_min_length 1024;
[server.response]
compression = true
compression_min_size = 1024

Custom headers:

add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
[server.response]
headers = [
    ["X-Frame-Options", "SAMEORIGIN"],
    ["X-Content-Type-Options", "nosniff"],
]

Client body size:

client_max_body_size 64m;
[server.request]
max_body_size = 67108864

4. TLS / HTTPS

Nginx + certbot:

server {
    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

ePHPm with automatic ACME:

[server.tls]
acme_domains = ["example.com"]
acme_email = "you@example.com"

Or with existing certificates:

[server.tls]
cert = "/etc/letsencrypt/live/example.com/fullchain.pem"
key = "/etc/letsencrypt/live/example.com/privkey.pem"

5. Reverse Proxy / Upstream

If Nginx is proxying to multiple PHP-FPM pools or other backends, ePHPm doesn’t replace that. For pure PHP serving (the most common case), ePHPm replaces both Nginx and PHP-FPM. If you need Nginx as a reverse proxy to non-PHP services, keep Nginx for those and point it at ePHPm for PHP.

6. Switch Over

# Stop nginx + php-fpm
sudo systemctl stop nginx php8.2-fpm

# Install ePHPm as a system service (registers + starts it)
sudo ephpm install

# Verify
curl http://localhost:8080

# Disable the old stack from boot
sudo systemctl disable nginx php8.2-fpm

What You Gain

Nginx + PHP-FPMePHPm
Services21
Config filesnginx.conf + site config + php-fpm.conf + pool configOne ephpm.toml
PHP ↔ HTTP overheadFastCGI serialization over Unix socketZero (in-process)
Cold startFPM spawns new workers on demandWorkers always warm
Memory (idle)~100 MB (Nginx) + ~150 MB (FPM pool)~30 MB
TLSNginx + certbot + cronBuilt-in, automatic
PHP version upgradeapt install, restart FPMDownload new binary
Log filesNginx access/error + FPM error + PHP errorOne log stream

What You Lose

  • Nginx as a reverse proxy — if you’re proxying to non-PHP backends (Node, Python, etc.), you still need a reverse proxy for those.
  • Multiple FPM pools — ePHPm has one shared thread pool. If you run separate pools for different sites with different users, use ePHPm’s virtual hosts instead (same isolation, simpler config).
  • Nginx modulesngx_pagespeed, ngx_brotli, etc. Most functionality is built into ePHPm or handled at the application level.
  • HTTP/3 (QUIC) — not yet implemented in ePHPm. Nginx supports it via nginx-quic.

Laravel-Specific Notes

Laravel’s Nginx config:

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

ePHPm:

[server]
document_root = "/var/www/laravel/public"
fallback = ["$uri", "$uri/", "/index.php?$query_string"]

The document_root points to public/ — same as your Nginx root directive. Everything else works automatically.