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"