Courses Developer Fundamentals for Builders Nginx, PHP-FPM, and OPcache

How Your Stack Actually Works

Nginx, PHP-FPM, and OPcache

The three layers between a request and your Laravel code

13 min read · Lesson 2 of 18

The Server Stack You're Running

When an HTTP request arrives at your DigitalOcean droplet (or any server running Laravel Forge), it passes through three layers before your PHP code executes. Understanding these layers explains most of the mysterious production issues you'll encounter.


Nginx: The Front Door

Nginx (pronounced "engine-x") is a web server and reverse proxy. It's the first process that receives incoming HTTP requests on ports 80 (HTTP) and 443 (HTTPS).

Nginx does NOT execute PHP code. It handles:

  • TLS termination — Decrypts HTTPS connections using your SSL certificate.
  • Static file serving — CSS, JS, images, and fonts are served directly by Nginx without touching PHP. This is why your public/ directory exists.
  • Request routing — Decides whether a request should go to PHP-FPM or be served as a static file.
  • Load balancing — Can distribute requests across multiple backend servers.

Your Forge-generated Nginx config contains a critical block:

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

This says: "If the request is for a PHP file, forward it to PHP-FPM via a Unix socket." For Laravel, every request that isn't a static file gets routed to public/index.php through a try_files directive.

A common Forge gotcha: if Forge creates a default public/index.html placeholder file, Nginx serves it instead of index.php because HTML files take priority. Always delete that placeholder after deploying Laravel.

PHP-FPM: The Worker Pool

PHP-FPM (FastCGI Process Manager) is a pool of PHP worker processes waiting to execute your code. When Nginx forwards a request, one of these workers picks it up.

The key concepts:

  • Worker processes — Each worker handles one request at a time. If you have 10 workers and 15 simultaneous requests, 5 requests wait in a queue.
  • Process lifecycle — Workers are pre-spawned (they're sitting in memory, ready to go). This is much faster than starting a new PHP process per request.
  • Memory isolation — Each worker is independent. A crash in one worker doesn't affect others. This is why PHP's "shared nothing" architecture is so resilient.

PHP-FPM configuration controls how many workers run:

pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10

On a typical 2GB Forge server, each PHP-FPM worker uses roughly 30-60MB of RAM. With max_children = 20, that's up to 1.2GB of RAM just for PHP workers. This is why running out of memory causes 502 errors — Nginx tries to contact PHP-FPM, but all workers are busy or dead.


OPcache: The Speed Layer (and Deployment Trap)

OPcache is PHP's built-in bytecode cache. It takes your PHP source code, compiles it to bytecode (an intermediate representation the PHP engine can execute directly), and caches that bytecode in shared memory.

Without OPcache, every request re-reads and re-compiles your PHP files. With OPcache, the compilation happens once, and subsequent requests use the cached bytecode. This typically improves performance by 30-70%.

The problem: OPcache doesn't know when your files change.

By default in production, OPcache has opcache.validate_timestamps=0, meaning it never checks if your PHP files have been modified. When you deploy new code via git pull, OPcache keeps serving the old bytecode until it's manually cleared.

This is the most common "I deployed but nothing changed" problem. The solutions:

  • php artisan optimize:clear — Clears Laravel's internal caches (config, routes, views) but does NOT clear OPcache.
  • Restart PHP-FPMsudo systemctl restart php8.3-fpm kills all workers and starts fresh ones, which forces OPcache to recompile. Requires sudo access.
  • opcache_reset() — A PHP function that clears OPcache. You need to call it from a web request (not CLI), because CLI and FPM have separate OPcache pools.
If you can't restart PHP-FPM (no sudo access), the workaround is to create a temporary PHP file that calls opcache_reset(), curl it from the server, then delete it. It's hacky but effective.

How They Work Together

Here's the complete flow for a Laravel request:

  1. Nginx receives the HTTPS request, terminates TLS.
  2. Nginx checks: is this a static file? If yes, serve it directly. If no, forward to PHP-FPM.
  3. PHP-FPM assigns an available worker process.
  4. The worker loads public/index.php. OPcache provides the cached bytecode (or compiles it fresh if not cached).
  5. Laravel boots, processes the request, returns an HTML/JSON response.
  6. The response flows back through PHP-FPM → Nginx → client.
  7. The PHP-FPM worker resets and goes back to the pool, ready for the next request.

Key Takeaways

  • Nginx handles TLS, static files, and routing. It doesn't execute PHP.
  • PHP-FPM manages a pool of worker processes. Running out of workers causes 502 errors.
  • OPcache dramatically improves performance but caches stale code after deployments.
  • php artisan optimize:clear does NOT clear OPcache. You need to restart FPM or call opcache_reset().
  • Understanding this stack is the difference between debugging effectively and staring at "it works on my machine" for hours.
Ask about this lesson