Goals

  • Configure and run the right containers to tunnel requests into a PHP-FPM server through a Nginx webserver, all through our previously set Traefik reverse proxy

Introduction

The Specificity of PHP-FPM (FastCGI Process Manager)

The key is in the architecture’s approach to PHP.

Traditional Method: In a traditional setup (e.g. Apache with mod_php), the web server and the PHP interpreter are a single, monolithic process. Every time a new web request comes in, a new Apache process is created that contains the entire PHP engine. This is simple to set up, but it’s resource-intensive and doesn’t scale well, especially for large numbers of requests.

PHP-FPM Method: PHP-FPM separates the concerns. The web server (Nginx) is optimized for what it does best (serving files), and PHP-FPM is a separate, dedicated process pool optimized for running PHP. This allows each component to be highly performant and scalable. When the system is under heavy load, you can simply scale the PHP-FPM service without affecting the web server. This approach is much more efficient and resilient for modern web applications.

The Request Flow

A request enters Traefik on port 443 (HTTPS), which decrypts it and sends it to Nginx. Nginx, acting as a proxy, forwards the request to PHP-FPM on a private internal network. PHP-FPM processes the code, sends the output back to Nginx, and Nginx returns the final content back to Traefik, which then encrypts and delivers it to the user. As shown in the following diagram.

Diagram Overview of Traefik/Docker relationship Overview of the Docker - Traefik - Nginx - PHP-FPM Request Flow


Source code

Here you’ll find the source code for a basic PHP demo page showing how the different containers work together to handle requests: https://github.com/a-naitslimane/anaitslimane.article.php-fpm-traefik-docker


Setup

Prerequisites:

  • Docker and Docker Compose installed on your machine
  • A Dockerized Traefik reverse proxy running locally. If not, please follow through my previous article Traefik reverse proxy

Important!

The following configs are strongly linked to my previous article’s configs. Please remember to adjust with your own values. Especially the external docker network’s name

Local setup

  1. Clone the source code to your working-directory

  2. Set environment variables

    • Create a copy of .env.exemple named .env.

      In it, you can set your own values of course, as long as they are consistent with the rest of the following config.\

  3. Bind your DOMAIN_NAME to the localhost:

    • Add your local domain name to the hosts file: C:\Windows\System32\drivers\etc\hosts (on Windows) and /etc/hosts (on Linux).

      127.0.0.1 php-fpm.demo.domain
  4. Launch the services using docker compose:

    • In your terminal, first cd to your working-directory then type:
      docker compose -f docker-compose.yaml -f up

Testing

  • Check that both the nginx and php-fpm containers are running and healthy.

  • In your browser, open the address you’ve configured as DOMAIN_NAME (ex: https://php-fpm.demo.domain )
    You should be seeing the following page:

    Traefik Dashboard


Dissection

  • Here are defined the docker’s networks:

    ## docker-compose.yaml ##
    3networks:
    4    my-external-network:
    5        external: true
    6    my-internal-network:
    7        external: false
  • The my-external-network is the only one exposed to the outside and is used by the nginx container to communicate with Traefik:

    ## docker-compose.yaml ##
     9services:
    10    nginx:
    11        image: nginx:alpine
    12        container_name: nginx
    13        volumes:
    14            # Mount the application source code into the Nginx container.
    15            - ./public:/var/www/html
    16            # Mount the custom Nginx configuration file.
    17            - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    18        networks:
    19            - my-external-network
    20            - my-internal-network
  • The other one (my-internal-network) is found in both nginx and php-fpm containers and is used only for a communication between the two:

    ## docker-compose.yaml ##
    31    php-fpm:
    32        image: php:8.3-fpm-alpine
    33        container_name: php-fpm
    34        volumes:
    35            # Mount the application source code into the PHP-FPM container.
    36            - ./public:/var/www/html
    37        networks:
    38            - my-internal-network
  • In the Nginx configuration, the highlighted line is probably the most important one. It indicates which docker host should receive *.php requests, and on which port

    ## default.conf ##
    14    # This location block specifically handles files ending in .php.
    15    location ~ \.php$ {
    16        # Ensure the file exists to prevent arbitrary code execution.
    17        try_files $uri =404;
    18
    19        # Pass the request to the PHP-FPM service.
    20        fastcgi_pass php-fpm:9000;
  • Here is a simple test showing that php-fpm is set to processing requests

    ## index.php ##
    54        <?php
    55            ob_start();
    56            phpinfo();
    57            $php_info = ob_get_clean();
    58
    59            $lines = explode("\n", $php_info);
    60            foreach ($lines as $line) {
    61                if (strpos($line, 'Server API') !== false) {
    62                    echo htmlspecialchars($line);
    63                    break;
    64                }
    65            }
    66        ?>
    

Wrapping up

As we’ve seen, setting up a modern web stack with Docker, Traefik, Nginx, and PHP-FPM is a powerful way to build a robust and scalable application. It represents a fundamental shift away from monolithic, all-in-one web servers to a more efficient, decoupled architecture.

This separation of concerns is the key benefit. It means you can scale each part of your application independently, ensuring your website remains performant and resilient under heavy load.