Resumen: configuración práctica para escalar una aplicación Symfony en Docker con Nginx como proxy, tres (o más) contenedores PHP‑FPM y Memcached para sesiones compartidas.
El artículo describe la arquitectura, los archivos clave, comandos para levantar el entorno y pruebas para verificar persistencia de sesión entre contenedores.
Introducción
Cuando una aplicación Symfony supera la capacidad de un solo contenedor PHP‑FPM, la solución es ejecutar múltiples instancias de PHP‑FPM y balancearlas con Nginx. Para que las sesiones funcionen entre instancias se requiere un almacén centralizado: Memcached.
Prerrequisitos
Antes de empezar, confirma lo siguiente en tu entorno:
- Docker y Docker Compose instalados en la máquina host.
- Una aplicación Symfony ubicada en ./src dentro del proyecto.
- Conocimientos básicos de Nginx, PHP‑FPM y configuración de servicios en contenedores.
Desarrollo
La arquitectura recomendada mínima incluye: un contenedor Nginx (proxy/load‑balancer), tres contenedores PHP‑FPM idénticos, un contenedor Memcached y un contenedor PostgreSQL. Nginx distribuye peticiones entre los PHP‑FPM y Memcached almacena sesiones.
project/
├── docker/
│ ├── nginx/
│ │ ├── Dockerfile
│ │ └── default.conf
│ ├── php-fpm/
│ │ ├── Dockerfile
│ │ └── php.ini
│ └── memcached/
│ └── memcached.conf
├── src/ (tu aplicación Symfony)
├── docker-compose.yml
└── .env
Lenguaje del código: texto plano (plaintext)
Procedimiento
Los pasos esenciales: definir docker-compose, configurar Nginx upstream, preparar la imagen PHP‑FPM con memcached y opcache, ajustar php.ini y la pool de PHP‑FPM, configurar Symfony para usar Memcached como handler de sesión y levantar el conjunto.
version: '3.8'
services:
nginx:
build:
context: ./docker/nginx
dockerfile: Dockerfile
ports:
- "80:80"
volumes:
- ./src:/var/www/symfony
- php-fpm-sockets1:/var/run/sock/fpm1
- php-fpm-sockets2:/var/run/sock/fpm2
- php-fpm-sockets3:/var/run/sock/fpm3
depends_on:
- php-fpm-1
- php-fpm-2
- php-fpm-3
networks:
- symfony-network
php-fpm-1:
build:
context: ./docker/php-fpm
dockerfile: Dockerfile
volumes:
- ./src:/var/www/symfony
- php-fpm-sockets1:/var/run/sock:rw
environment:
- APP_ENV=prod
- DATABASE_URL=postgresql://symfony:secret@postgres:5432/symfony_db
- MEMCACHED_HOST=memcached
- MEMCACHED_PORT=11211
depends_on:
- memcached
- postgres
networks:
- symfony-network
php-fpm-2:
build:
context: ./docker/php-fpm
dockerfile: Dockerfile
volumes:
- ./src:/var/www/symfony
- php-fpm-sockets2:/var/run/sock:rw
environment:
- APP_ENV=prod
- DATABASE_URL=postgresql://symfony:secret@postgres:5432/symfony_db
- MEMCACHED_HOST=memcached
- MEMCACHED_PORT=11211
depends_on:
- memcached
- postgres
networks:
- symfony-network
php-fpm-3:
build:
context: ./docker/php-fpm
dockerfile: Dockerfile
volumes:
- ./src:/var/www/symfony
- php-fpm-sockets3:/var/run/sock:rw
environment:
- APP_ENV=prod
- DATABASE_URL=postgresql://symfony:secret@postgres:5432/symfony_db
- MEMCACHED_HOST=memcached
- MEMCACHED_PORT=11211
depends_on:
- memcached
- postgres
networks:
- symfony-network
memcached:
image: memcached:1.6-alpine
ports:
- "11211:11211"
command: memcached -m 256
networks:
- symfony-network
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_DB=symfony_db
- POSTGRES_USER=symfony
- POSTGRES_PASSWORD=secret
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- symfony-network
networks:
symfony-network:
driver: bridge
volumes:
postgres-data:
php-fpm-sockets1:
php-fpm-sockets2:
php-fpm-sockets3:
Lenguaje del código: YAML (yaml)
FROM nginx:1.25-alpine
COPY default.conf /etc/nginx/conf.d/default.conf
RUN mkdir -p /var/www/symfony
WORKDIR /var/www/symfony
EXPOSE 80
Lenguaje del código: Dockerfile (dockerfile)
upstream php_fpm_backend {
least_conn;
server unix:/var/run/sock/fpm1/php-fpm.sock weight=1 max_fails=3 fail_timeout=30s;
server unix:/var/run/sock/fpm2/php-fpm.sock weight=1 max_fails=3 fail_timeout=30s;
server unix:/var/run/sock/fpm3/php-fpm.sock weight=1 max_fails=3 fail_timeout=30s;
}
server {
listen 80;
server_name localhost;
root /var/www/symfony/public;
index index.php;
client_max_body_size 20M;
client_body_buffer_size 128k;
location / {
try_files $uri /index.php$is_args$args;
}
location ~ ^/index\.php(/|$) {
fastcgi_pass php_fpm_backend;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
fastcgi_read_timeout 300;
fastcgi_send_timeout 300;
fastcgi_intercept_errors off;
internal;
}
location ~ \.php$ {
return 404;
}
error_log /var/log/nginx/symfony_error.log;
access_log /var/log/nginx/symfony_access.log;
}
Lenguaje del código: Nginx (nginx)
FROM php:8.2-fpm-alpine
RUN apk add --no-cache \
postgresql-dev \
libzip-dev \
libmemcached-dev \
zlib-dev \
cyrus-sasl-dev \
git \
unzip
RUN docker-php-ext-install \
pdo \
pdo_pgsql \
opcache \
zip
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \
&& pecl install memcached-3.2.0 \
&& docker-php-ext-enable memcached \
&& apk del .build-deps
COPY php.ini /usr/local/etc/php/conf.d/custom.ini
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR /var/www/symfony
RUN addgroup -g 1000 symfony && adduser -D -u 1000 -G symfony symfony
RUN chown -R symfony:symfony /var/www/symfony
USER symfony
EXPOSE 9000
CMD ["php-fpm"]
Lenguaje del código: Dockerfile (dockerfile)
[PHP]
memory_limit = 512M
upload_max_filesize = 20M
post_max_size = 20M
max_execution_time = 300
max_input_time = 300
[opcache]
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0
opcache.revalidate_freq = 0
opcache.save_comments = 1
opcache.fast_shutdown = 1
[Session]
session.save_handler = memcached
session.save_path = "memcached:11211"
session.gc_maxlifetime = 3600
[Date]
date.timezone = Europe/Paris
Lenguaje del código: TOML, también INI (ini)
[www]
user = symfony
group = symfony
listen = 9000
listen.owner = symfony
listen.group = symfony
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 10
pm.max_spare_servers = 20
pm.max_requests = 500
pm.status_path = /fpm-status
ping.path = /fpm-ping
request_terminate_timeout = 300
request_slowlog_timeout = 10
slowlog = /var/log/php-fpm/slow.log
catch_workers_output = yes
decorate_workers_output = no
php_admin_value[error_log] = /var/log/php-fpm/error.log
php_admin_flag[log_errors] = on
Lenguaje del código: TOML, también INI (ini)
framework:
secret: '%env(APP_SECRET)%'
session:
handler_id: session.handler.memcached
cookie_secure: auto
cookie_samesite: lax
save_path: '%env(MEMCACHED_HOST)%:%env(MEMCACHED_PORT)%'
gc_maxlifetime: 3600
services:
session.handler.memcached:
class: Symfony\Component\HttpFoundation\Session\Storage\Handler\MemcachedSessionHandler
arguments:
- '@memcached.connection'
memcached.connection:
class: Memcached
calls:
- method: addServer
arguments:
- '%env(MEMCACHED_HOST)%'
- '%env(int:MEMCACHED_PORT)%'
- method: setOption
arguments:
- !php/const Memcached::OPT_PREFIX_KEY
- 'symfony_'
- method: setOption
arguments:
- !php/const Memcached::OPT_BINARY_PROTOCOL
- true
Lenguaje del código: YAML (yaml)
docker-compose build
docker-compose up -d
docker-compose ps
docker-compose exec php-fpm-1 composer install
docker-compose exec php-fpm-1 php bin/console doctrine:migrations:migrate --no-interaction
Lenguaje del código: Bash (bash)
Ejemplos
Controlador de ejemplo para verificar persistencia de sesión y cuál contenedor atiende la petición.
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class TestController extends AbstractController
{
#[Route('/test/session', name: 'test_session')]
public function testSession(Request $request): Response
{
$session = $request->getSession();
$counter = $session->get('counter', 0);
$counter++;
$session->set('counter', $counter);
$hostname = gethostname();
return $this->json([
'container' => $hostname,
'session_id' => $session->getId(),
'counter' => $counter,
'timestamp' => time()
]);
}
}
Lenguaje del código: PHP (php)
Prueba desde la terminal usando cookies para mantener la sesión entre llamadas:
curl -c cookies.txt -b cookies.txt http://localhost/test/session
curl -c cookies.txt -b cookies.txt http://localhost/test/session
curl -c cookies.txt -b cookies.txt http://localhost/test/session
Lenguaje del código: Bash (bash)
Checklist
- Verificar que todos los PHP‑FPM apunten al mismo Memcached (host/puerto).
- Asegurar permisos de lectura/escritura sobre sockets compartidos si se usan sockets Unix.
- Configurar health checks (fpm-ping, nginx stub_status) y revisar logs.
- Ajustar pm.max_children según RAM disponible y uso por worker.
- Usar .env o secretos para credenciales y no versionarlas.
Conclusión
Con Nginx como balanceador, múltiples contenedores PHP‑FPM y Memcached para sesiones puedes escalar horizontalmente una aplicación Symfony sin cambios en el código de sesión.
Comienza con tres contenedores PHP‑FPM, monitoriza CPU/memoria y ajusta pm.* o añade contenedores según la demanda. Implementa health checks y protege credenciales en producción.

Deja un comentario