--- title: "Sesiones persistentes (sticky) con HAProxy y Nginx" description: "Configura sesiones persistentes en Ubuntu 24.04 con cookies en HAProxy, ip_hash y cookies de ruta en Nginx, y comprueba con curl que funcionan." category: "Proxy y Equilibrio de Carga" language: es source: https://cubepath.com/es/docs/proxy-y-equilibrio-de-carga/configuracion-de-sesiones-pegajosas-en-equilibradores-de-carga updated: 2026-09-25 --- # Sesiones persistentes (sticky) con HAProxy y Nginx Un balanceador reparte las peticiones entre varios servidores, pero muchas aplicaciones guardan la sesión del usuario (carrito, login, formularios a medias) en la memoria del servidor que la creó. Si la siguiente petición llega a otro servidor, el usuario pierde la sesión. Las sesiones persistentes, o *sticky sessions*, hacen que el balanceador envíe siempre al mismo cliente al mismo backend. En esta guía montarás dos backends de prueba en Ubuntu 24.04 y configurarás la persistencia por cookie en HAProxy, y por IP y por cookie en Nginx, comprobando cada caso con `curl`. ## Requisitos previos - Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath. - Un usuario no root con privilegios `sudo`. - Los puertos 80 y 8000 TCP abiertos para las pruebas: `sudo ufw allow 80,8000/tcp`. Para que la guía se pueda seguir en un solo servidor, los dos backends son pequeños servidores web locales en los puertos 8081 y 8082. En producción serán servidores distintos: cambia `127.0.0.1:8081` y `127.0.0.1:8082` por sus IP privadas. ## Métodos de persistencia | Método | Cómo identifica al cliente | Ventajas | Inconvenientes | |---|---|---|---| | Cookie insertada por el balanceador | Cookie propia (`SERVERID`) con el nombre del backend | Precisa, independiente de la IP | Requiere HTTP (o terminar TLS en el balanceador) | | Hash de IP de origen | IP del cliente | Funciona sin cookies, también en TCP | Muchos usuarios tras la misma IP (NAT, oficinas) caen en el mismo backend; si la IP del cliente cambia, pierde la sesión | | Tabla de afinidad (stick-table) | IP u otro dato guardado en memoria del balanceador | Flexible, con caducidad configurable | Se pierde al reiniciar si no se sincroniza entre balanceadores | ## Paso 1: Preparar los backends de prueba Instala HAProxy y Nginx: ```bash sudo apt update sudo apt install haproxy nginx ``` Nginx trae un sitio por defecto en el puerto 80, que usará HAProxy en esta guía. Desactívalo: ```bash sudo rm /etc/nginx/sites-enabled/default sudo systemctl reload nginx ``` Crea una página distinta para cada backend, de forma que la respuesta indique quién la sirvió: ```bash sudo mkdir -p /srv/app1 /srv/app2 echo "app1" | sudo tee /srv/app1/index.html echo "app2" | sudo tee /srv/app2/index.html ``` Arranca dos servidores web con `systemd-run`, que los ejecuta como servicios temporales de systemd: ```bash sudo systemd-run --unit=demo-app1 /usr/bin/python3 -m http.server 8081 --bind 127.0.0.1 --directory /srv/app1 sudo systemd-run --unit=demo-app2 /usr/bin/python3 -m http.server 8082 --bind 127.0.0.1 --directory /srv/app2 ``` Comprueba que responden: ```bash curl -s http://127.0.0.1:8081/ curl -s http://127.0.0.1:8082/ ``` ```output app1 app2 ``` ## Paso 2: Persistencia por cookie en HAProxy HAProxy puede insertar su propia cookie con el nombre del servidor que atendió la primera petición. En las siguientes peticiones lee la cookie y envía al cliente al mismo servidor, sin que la aplicación tenga que hacer nada. Abre la configuración: ```bash sudo nano /etc/haproxy/haproxy.cfg ``` Deja las secciones `global` y `defaults` que trae Ubuntu y añade al final: ```haproxy frontend fe_web bind :80 default_backend be_app backend be_app balance roundrobin option redispatch option httpchk GET / cookie SERVERID insert indirect nocache httponly server app1 127.0.0.1:8081 check cookie app1 server app2 127.0.0.1:8082 check cookie app2 ``` Qué hace cada línea relevante: - `cookie SERVERID insert`: HAProxy añade la cookie `SERVERID` en la respuesta. - `indirect`: la cookie no se reenvía a la aplicación ni se vuelve a enviar al cliente si ya la tiene. - `nocache`: añade `Cache-Control: private` a las respuestas con la cookie, para que una caché intermedia no la sirva a otros usuarios. - `httponly`: el JavaScript de la página no puede leerla. Si terminas TLS en HAProxy, añade también `secure`. - `cookie app1` en cada `server`: el valor que se guarda en la cookie para ese servidor. - `option httpchk` y `check`: comprobaciones de salud HTTP. Si un servidor cae, HAProxy ignora las cookies que apuntan a él y reparte esos clientes entre los demás. - `option redispatch`: si la conexión al servidor de la cookie falla antes de que la comprobación de salud lo marque como caído, reintenta en otro. Valida y recarga: ```bash sudo haproxy -c -f /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy ``` ```output Configuration file is valid ``` ### Comprobar la persistencia en HAProxy Sin cookie, las peticiones se reparten por turnos: ```bash for i in 1 2 3 4; do curl -s http://127.0.0.1/; done ``` ```output app1 app2 app1 app2 ``` Mira la cabecera que inserta HAProxy en la primera respuesta: ```bash curl -sI http://127.0.0.1/ | grep -i -e set-cookie -e cache-control ``` ```output set-cookie: SERVERID=app2; path=/; HttpOnly cache-control: private ``` Ahora guarda y reenvía la cookie como haría un navegador. Todas las peticiones llegan al mismo backend: ```bash rm -f ~/cookies.txt for i in 1 2 3 4; do curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1/; done ``` ```output app1 app1 app1 app1 ``` Para probar la conmutación, detén el backend que te ha tocado, espera unos segundos a que la comprobación de salud lo detecte (por defecto, tres fallos seguidos a intervalos de 2 segundos) y repite la petición con la misma cookie: ```bash sudo systemctl stop demo-app1 sleep 10 curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1/ ``` ```output app2 ``` HAProxy ha enviado la petición a `app2` y ha reescrito la cookie, así que el cliente queda fijado al nuevo servidor. Vuelve a arrancar el backend para los siguientes pasos: ```bash sudo systemd-run --unit=demo-app1 /usr/bin/python3 -m http.server 8081 --bind 127.0.0.1 --directory /srv/app1 ``` ### Alternativa sin cookies: stick-table por IP Si el tráfico no es HTTP o no puedes usar cookies, HAProxy puede recordar qué servidor atendió a cada IP en una tabla en memoria. Sustituye la línea `cookie` del backend y el parámetro `cookie` de los servidores por: ```haproxy backend be_app balance roundrobin option httpchk GET / stick-table type ip size 200k expire 30m stick on src server app1 127.0.0.1:8081 check server app2 127.0.0.1:8082 check ``` Cada IP queda asociada a un servidor durante 30 minutos desde su última petición. Puedes consultar la tabla en tiempo real a través del socket de administración que configura Ubuntu: ```bash echo "show table be_app" | sudo socat stdio /run/haproxy/admin.sock ``` `socat` se instala con `sudo apt install socat` si no lo tienes. ## Paso 3: Persistencia por IP en Nginx La versión libre de Nginx no incluye la directiva `sticky`, que es exclusiva de NGINX Plus. La forma más sencilla de persistencia es `ip_hash`, que elige el backend a partir de la IP del cliente. En esta guía Nginx escuchará en el puerto 8000 para convivir con HAProxy. ```bash sudo nano /etc/nginx/sites-available/sticky-demo ``` ```nginx upstream app_iphash { ip_hash; server 127.0.0.1:8081; server 127.0.0.1:8082; } server { listen 8000; location / { proxy_pass http://app_iphash; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` Actívalo y recarga: ```bash sudo ln -s /etc/nginx/sites-available/sticky-demo /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ``` Todas las peticiones desde la misma IP llegan al mismo backend, sin cookies: ```bash for i in 1 2 3 4; do curl -s http://127.0.0.1:8000/; done ``` ```output app2 app2 app2 app2 ``` Con IPv4, `ip_hash` usa solo los tres primeros octetos de la dirección, así que todos los clientes de una misma red /24 van al mismo servidor. Si Nginx está detrás de otro proxy o de un CDN, todos los clientes compartirán su IP y la carga no se repartirá; en ese caso usa `hash $http_x_forwarded_for consistent;` o la persistencia por cookie del siguiente paso. ## Paso 4: Persistencia por cookie en Nginx Sin NGINX Plus puedes conseguir persistencia por cookie combinando dos `map`: uno que traduce la cookie `srv` a la dirección del backend y otro que, tras la respuesta, genera la cookie a partir del backend que realmente atendió la petición (`$upstream_addr`). Sustituye el contenido de `/etc/nginx/sites-available/sticky-demo`: ```bash sudo nano /etc/nginx/sites-available/sticky-demo ``` ```nginx upstream app_pool { server 127.0.0.1:8081; server 127.0.0.1:8082; } map $cookie_srv $sticky_target { app1 127.0.0.1:8081; app2 127.0.0.1:8082; default app_pool; } map $upstream_addr $srv_cookie { ~127\.0\.0\.1:8081$ "srv=app1; Path=/; HttpOnly"; ~127\.0\.0\.1:8082$ "srv=app2; Path=/; HttpOnly"; default ""; } server { listen 8000; location / { proxy_pass http://$sticky_target; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; add_header Set-Cookie $srv_cookie; error_page 502 504 = @fallback; } location @fallback { proxy_pass http://app_pool; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; add_header Set-Cookie $srv_cookie; } } ``` Cómo funciona: - Sin cookie, `$sticky_target` vale `app_pool` y Nginx reparte con el balanceo normal del upstream. - Con `srv=app1`, `proxy_pass` va directamente a `127.0.0.1:8081`. - `add_header Set-Cookie $srv_cookie` fija la cookie según el backend que respondió. Si Nginx probó varios servidores, `$upstream_addr` contiene la lista completa, por eso el `map` usa expresiones regulares ancladas al final (`$`) para quedarse con el último, el que respondió. Cuando el valor está vacío, Nginx no añade la cabecera. - Si el backend fijado no responde, `error_page` pasa la petición a `@fallback`, que usa el grupo completo y reescribe la cookie hacia el servidor que sí funciona. > Nota: con `proxy_pass` a una dirección concreta Nginx no aplica los reintentos del upstream. El bloque `@fallback` cubre ese caso, pero reenvía la petición completa, incluidas las POST. Si tus peticiones no son idempotentes, tenlo en cuenta o usa HAProxy. Comprueba y recarga: ```bash sudo nginx -t sudo systemctl reload nginx ``` Repite la prueba con un archivo de cookies: ```bash rm -f ~/cookies.txt for i in 1 2 3 4; do curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1:8000/; done ``` ```output app1 app1 app1 app1 ``` Y comprueba la conmutación deteniendo el backend fijado: ```bash sudo systemctl stop demo-app1 curl -s -b ~/cookies.txt -c ~/cookies.txt http://127.0.0.1:8000/ grep srv ~/cookies.txt ``` ```output app2 #HttpOnly_127.0.0.1 FALSE / FALSE 0 srv app2 ``` ## Paso 5: Limpiar el entorno de pruebas El backend `demo-app1` ya está detenido desde la prueba anterior. Detén el otro y elimina el sitio de Nginx: ```bash sudo systemctl stop demo-app2 sudo rm /etc/nginx/sites-enabled/sticky-demo sudo systemctl reload nginx ``` Los servicios creados con `systemd-run` son temporales y desaparecen al detenerse. Si solo quieres uno de los dos balanceadores, desinstala el otro. ## ¿Necesitas realmente sesiones persistentes? La persistencia resuelve un problema de la aplicación, no del balanceador, y tiene costes: la carga se reparte peor, y cuando un servidor cae o lo retiras para actualizarlo, sus usuarios pierden la sesión igualmente. La alternativa robusta es que la aplicación guarde las sesiones fuera del proceso, en Redis, Memcached o la base de datos. La mayoría de frameworks lo soportan con un cambio de configuración (por ejemplo, `SESSION_DRIVER=redis` en Laravel o `session.save_handler = redis` en PHP con la extensión phpredis). Con sesiones compartidas, cualquier backend puede atender cualquier petición y puedes quitar la persistencia del balanceador. Las sesiones persistentes siguen siendo útiles para aplicaciones heredadas que no puedes modificar, para WebSockets o conexiones largas con estado y para aprovechar cachés locales de cada servidor. ## Solución de problemas - **Las peticiones siguen alternando entre backends.** El cliente no devuelve la cookie. Con `curl` usa `-b` y `-c`; en el navegador revisa en las herramientas de desarrollo que la cookie se guarda. Si terminas TLS en el balanceador y la cookie tiene `secure`, no se envía por HTTP. - **Todo el tráfico va a un solo backend con `ip_hash`.** Los clientes llegan a través de un proxy, CDN o NAT y comparten IP. Usa persistencia por cookie. - **`show table` devuelve `Unknown command` o `Permission denied`.** El socket de administración no está configurado con `level admin` en la sección `global` o no ejecutas el comando con `sudo`. - **Un usuario pierde la sesión tras un despliegue.** Es el comportamiento esperado al reiniciar su backend. Pon el servidor en modo `drain` antes de retirarlo (`echo "set server be_app/app1 state drain" | sudo socat stdio /run/haproxy/admin.sock`) para que no reciba clientes nuevos mientras terminan los existentes, o externaliza las sesiones. ## Conclusión Has configurado persistencia de sesión por cookie y por IP en HAProxy, y por IP y por cookie en Nginx, y has comprobado que los clientes vuelven a su backend y conmutan a otro cuando cae. Como siguientes pasos, termina TLS en el balanceador y añade `secure` a las cookies, ajusta las comprobaciones de salud a un endpoint real de tu aplicación y valora mover las sesiones a Redis para poder prescindir de la persistencia.