Optimal Docker Scaling: Nginx Load Balancing with Multiple PHP-FPM Containers


2 views

When working with Dockerized PHP applications, we often face this architectural dilemma: Nginx needs to communicate with multiple PHP-FPM containers while maintaining port consistency. The traditional approach using direct container linking breaks when scaling PHP-FPM instances.

# Current problematic setup
services:
  php:
    image: php:8.2-fpm
    ports:
      - "9000:9000"  # All instances try to bind to same host port
  
  nginx:
    image: nginx:latest
    links:
      - php

We need to implement two key components:

1. Docker Compose with Scale Parameter

version: '3.8'
services:
  php:
    image: custom/php-fpm
    expose:
      - "9000"  # Internal port only
    deploy:
      replicas: 3
  
  nginx:
    image: custom/nginx
    ports:
      - "80:80"
    depends_on:
      - php

2. Nginx Configuration with DNS Round-Robin

Modern Docker versions provide built-in DNS resolution that handles container scaling automatically:

upstream php_servers {
    server php:9000;  # Docker's internal DNS handles multiple containers
}

server {
    location ~ \.php$ {
        fastcgi_pass php_servers;
        include fastcgi_params;
    }
}

For production environments, consider these enhancements:

upstream php_servers {
    least_conn;  # Better than round-robin for PHP
    server php:9000 max_fails=3 fail_timeout=30s;
    keepalive 16;  # Reuse connections between Nginx and PHP-FPM
}
services:
  php:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/ping"]
      interval: 30s
      timeout: 10s
      retries: 3

When working with Dockerized PHP applications, the traditional setup of linking a single Nginx container to a single PHP-FPM container works perfectly. However, production environments demand horizontal scaling, which introduces port conflicts when multiple PHP-FPM containers all try to expose port 9000.

# Problematic configuration when scaling
services:
  php:
    image: custom/php-fpm
    ports:
      - "9000"  # All replicas will clash on this port

The modern approach involves using Docker's built-in DNS resolution combined with Nginx's upstream module. Here's how to implement it:

# docker-compose.yml v3
version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    depends_on:
      - php
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf

  php:
    image: your/php-fpm
    deploy:
      replicas: 3
    expose:
      - "9000"  # Internal port only

Configure Nginx to use DNS round-robin load balancing:

# nginx.conf
upstream php_servers {
    server php:9000;
    server php:9000;
    server php:9000;
}

server {
    location ~ \.php$ {
        fastcgi_pass php_servers;
        # Other fastcgi params...
    }
}

For production environments, consider these enhancements:

upstream php_servers {
    least_conn;  # Use least connections algorithm
    server php:9000 max_fails=3 fail_timeout=30s;
    server php:9000 max_fails=3 fail_timeout=30s;
    server php:9000 max_fails=3 fail_timeout=30s;
    
    # For zero-downtime deployments
    keepalive 32;
}

Implement container health monitoring:

# In docker-compose.yml
services:
  php:
    healthcheck:
      test: ["CMD", "pgrep", "php-fpm"]
      interval: 30s
      timeout: 10s
      retries: 3

# Corresponding Nginx config
upstream php_servers {
    server php:9000 max_fails=3 fail_timeout=30s;
    server php:9000 max_fails=3 fail_timeout=30s;
    server php:9000 max_fails=3 fail_timeout=30s;
    
    # Only send traffic to healthy instances
    check interval=5000 rise=2 fall=3 timeout=1000;
}

For complex microservices architectures, consider using a service mesh like Linkerd or Istio:

# Example with Traefik as reverse proxy
services:
  reverse-proxy:
    image: traefik:v2.4
    ports:
      - "80:80"
    command:
      - --entrypoints.web.address=:80
      - --providers.docker

  php:
    image: your/php-fpm
    deploy:
      replicas: 3
    labels:
      - "traefik.http.routers.php.rule=PathPrefix(/)"
      - "traefik.http.services.php.loadbalancer.server.port=9000"