Cómo instalar HAProxy con Docker Compose en Ubuntu VPS
Un servicio web en un VPS es fácil de exponer directamente — hasta que quieres una puerta frontal pública limpia, la libertad de cambiar el backend más tarde, o una forma más segura de dejar de enviar tráfico a algo roto. Ese es el punto donde un proxy deja de parecer “algo para grandes equipos de infraestructura” y comienza a parecer práctico.

HAProxy encaja bien en ese rol. Piénsalo como el gestor de tráfico sentado frente a tu aplicación: las solicitudes llegan primero a HAProxy, y HAProxy decide a dónde deberían ir después. No necesitas un clúster grande para beneficiarte de eso. Incluso en un VPS Ubuntu 24.04, te da un borde más limpio entre internet y el servicio que realmente estás ejecutando.
Esta guía mantiene el primer despliegue intencionalmente estructurado: un VPS Ubuntu 24.04, Docker Compose, un contenedor HAProxy, un backend de demostración, y prueba de que el enrutamiento realmente funciona.
Por qué HAProxy es importante antes de que lo necesites
Imagina un pequeño VPS ejecutando una aplicación sin problemas hoy. Responde en un puerto, el sitio se carga y todo se ve bien. La fricción comienza cuando deseas un punto de entrada público estable, la opción de reemplazar el backend más tarde sin cambiar la dirección pública, o una capa frontal que pueda dejar de enviar tráfico a un servicio que falla. Exponer la aplicación directamente comienza a parecer frágil sorprendentemente rápido.

Esos requisitos apuntan todos a la misma capa faltante: un punto de entrada controlado entre internet y tu aplicación. HAProxy proporciona esa capa. Los clientes se conectan primero a HAProxy, y HAProxy decide a dónde va cada solicitud a continuación.
Esa separación es útil incluso antes de tener múltiples servidores. Te da un borde público más limpio ahora y un camino más seguro para cambios posteriores como reemplazo de backend, enrutamiento consciente de salud e HTTPS. El resto de la guía muestra ese patrón en su forma de trabajo más simple y lo verifica con una ruta de solicitud real.
Términos Rápidos de HAProxy Que Facilitan el Resto de Esta Guía

Solo necesitas un pequeño conjunto de vocabulario para seguir con confianza un primer despliegue de HAProxy. La tabla a continuación cubre los términos que importan en esta guía.
| Término | Significado en lenguaje simple |
|---|---|
| 🌐 reverse proxy | Un servicio frontal que recibe solicitudes primero y las pasa a otro servicio interno. |
| ⚖️ load balancer | Una capa frontal que puede distribuir solicitudes entre más de un destino backend. |
| 🚪 frontend | El lugar donde los clientes se conectan a HAProxy. |
| 🧩 backend | El servicio o servidor al que HAProxy envía la solicitud a continuación. |
| ❤️ health check | Una forma para que HAProxy note si un backend debe seguir recibiendo tráfico. |
| 🐳 image | Una plantilla de aplicación empaquetada utilizada para crear contenedores. |
| 📦 container | Una instancia en ejecución de una imagen. |
Para esta guía, reverse proxy es el primer modelo mental a tener en cuenta. HAProxy se sitúa frente a algo más y controla la transferencia. Load balancing es la capacidad extendida que se vuelve útil cuando agregas múltiples servidores backend más adelante.
Los dos términos que más importan una vez que abres la configuración son frontend y backend. El frontend es donde llega el cliente. El backend es donde HAProxy envía la solicitud a continuación. Un health check importa porque permite que HAProxy note cuándo un destino debe dejar de recibir tráfico.
Lo que HAProxy Hace Bien — y Lo que Esta Guía Intencionalmente Omite

Si imaginas tu stack como un edificio de oficinas, HAProxy es la recepción: el tráfico llega allí primero, se dirige a la sala correcta, y deja de enviarse a una sala que claramente no está disponible.
En esta guía, eso se traduce en tres trabajos relevantes para principiantes:
- aceptar solicitudes HTTP entrantes
- reenviarlas al backend de demostración
- monitorear si ese backend está lo suficientemente saludable para seguir recibiendo tráfico
Esto ya es útil con un backend porque te da un borde público controlado frente a la aplicación.
Más adelante, el mismo patrón se escala limpiamente. Puedes reemplazar el backend, agregar más backends, introducir HTTPS, o dejar que HAProxy distribuya el tráfico entre múltiples objetivos en lugar de solo uno. Para mantener el primer paso enseñable, esta guía se mantiene en modo HTTP e intencionalmente omite terminación TLS, ACLs, limitación de velocidad, tablas sticky, y pares HA. Todos esos son temas reales de HAProxy. Solo que no son el punto de partida correcto para un primer despliegue funcional.
Lo que estás construyendo y lo que necesitas primero

Antes de crear archivos, es útil ver la forma final del stack. El despliegue en esta guía se ve así:
Client browser or curl
|
v
HAProxy frontend (:80)
|
v
demo backend service (demo:5678)
Optional local-only validation:
HAProxy stats frontend (127.0.0.1:8404/stats)Docker Compose es el camino principal aquí porque mantiene la instalación inicial reproducible, visible y fácil de editar. En lugar de construir una imagen personalizada el primer día, mantienes la configuración de HAProxy en el host, la montas en el contenedor e inicias todo el stack desde un archivo. En un VPS Ubuntu autogestionado — por ejemplo, un VPS de AlexHost — es un ajuste limpio porque el diseño sigue siendo fácil de inspeccionar.
💡 Consejo: Esta guía utiliza Docker Compose más un haproxy.cfg montado por bind a propósito. Es el camino de primera instalación más transparente porque puedes editar la configuración del proxy directamente sin agregar un paso de construcción de imagen.
Antes de comenzar, asegúrate de tener estos conceptos básicos en su lugar:
- VPS Ubuntu 24.04
- Docker Engine instalado
- Docker Compose v2 disponible a través de docker compose
- Acceso a terminal y permiso para ejecutar Docker
- Puerto 80 disponible en el host
- HTTP entrante permitido si usas UFW o reglas de firewall del lado del proveedor
Primero, verifica tu versión de Ubuntu
lsb_release -a
A continuación, confirma que Docker y Compose moderno están disponibles:
docker --version
docker compose version
Si ambos comandos devuelven información de versión, el lado del runtime del contenedor está listo y puedes mantener el enfoque en HAProxy en lugar de desviarte hacia la instalación de Docker.
A continuación, asegúrate de que el puerto 80 no esté ya en uso, luego verifica si UFW está activo y si HTTP ya está permitido:
sudo ss -tlnp | grep -E ':(80)s' || true
sudo ufw status
sudo ufw allow 80/tcp
✏️ NOTA: Sin salida de la verificación de ss generalmente significa que el puerto 80 está libre. Si ves nginx, apache2, caddy u otro servicio ya escuchando allí, soluciona eso primero. Es un paso de verificación previa de diez segundos que evita mucha confusión más adelante.
En el ejemplo anterior, sudo ufw status muestra Status: active, y 80/tcp ya está presente en la lista de permitidos. Por eso sudo ufw allow 80/tcp devuelve Skipping adding existing rule en lugar de agregar una nueva. Esa salida es normal y simplemente significa que la regla del firewall ya estaba en su lugar.
Crear la Carpeta del Proyecto y el Archivo Compose
Comienza creando una pequeña carpeta de proyecto para los dos archivos que necesita este primer despliegue:
mkdir -p ~/haproxy-docker
cd ~/haproxy-docker
Después de eso, el diseño debe ser lo más pequeño posible:
~/haproxy-docker/
├── compose.yaml
└── haproxy.cfgAhora crea compose.yaml y usa este contenido exacto:
services:
demo:
image: hashicorp/http-echo:1.0
command: ["-listen=:5678", "-text=Hello from the HAProxy demo backend"]
restart: unless-stopped
haproxy:
image: haproxy:3.4.1
depends_on:
- demo
ports:
- "80:80"
- "127.0.0.1:8404:8404"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
sysctls:
net.ipv4.ip_unprivileged_port_start: "0"
restart: unless-stoppedEste archivo conecta los contenedores entre sí, pero aún no define la lógica de solicitudes de HAProxy. Le dice a Docker qué imágenes ejecutar, qué puertos publicar y dónde se montará la configuración de HAProxy desde el host.
Los siguientes parámetros son los que más importan para un primer despliegue limpio:
| Parámetro de Compose | Por qué está aquí |
|---|---|
| hashicorp/http-echo:1.0 | Te proporciona un backend de demostración diminuto y predecible sin enseñar un segundo servidor web al mismo tiempo. |
| haproxy:3.4.1 | Utiliza una etiqueta estable fijada en lugar de latest, lo que mantiene la guía menos frágil con el tiempo. |
| depends_on | Inicia el servicio demo antes de HAProxy, lo que es útil para el orden de la primera ejecución. |
| 80:80 | Publica el escucha HTTP principal en el puerto web estándar que esperan los lectores. |
| 127.0.0.1:8404:8404 | Mantiene la página de estadísticas disponible para validación local sin exponerla públicamente de forma predeterminada. |
| ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro | Monta tu archivo de configuración visible del lado del host en la imagen oficial de HAProxy como solo lectura. |
| sysctls con net.ipv4.ip_unprivileged_port_start: “0” | Permite que el contenedor HAProxy sin privilegios se vincule a puertos bajos como 80. |
| restart: unless-stopped | Te proporciona un valor predeterminado práctico de VPS: reinicia después de una falla o reinicio, pero respeta una parada manual intencional. |
Un detalle más importa aquí: no hay una red Docker personalizada en este archivo porque Docker Compose crea una red predeterminada automáticamente. Eso te proporciona DNS de nombre de servicio dentro del proyecto, por lo que HAProxy podrá alcanzar el backend como demo:5678 sin cableado adicional.
⚠️ Advertencia: El puerto 80 es un puerto privilegiado, por lo que la línea sysctls no es decorativa. Cambiar la asignación del host a 8080:80 no elimina el requisito de puerto privilegiado dentro del contenedor si HAProxy aún se vincula a :80 internamente.
Escribir y Validar un haproxy.cfg Mínimo
Con el cableado del contenedor en su lugar, HAProxy aún necesita instrucciones sobre dónde llega el tráfico, dónde debe ir y cómo se verifica la salud del backend. Crea haproxy.cfg a continuación:
global
log stdout format raw local0
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http
bind :80
default_backend demo_backend
backend demo_backend
balance roundrobin
server demo1 demo:5678 check
frontend stats
bind :8404
stats enable
stats refresh 10s
stats uri /statsEsta es una configuración mínima, pero no es una desechable. log stdout format raw local0 es la opción de registro compatible con contenedores porque Docker puede exponer stdout fácilmente, y mode http en defaults mantiene todo el ejemplo en modo HTTP para que el comportamiento del listener y del backend se mantengan consistentes y legibles.
✏️ NOTA: Un detalle vale la pena destacar antes del desglose de secciones: balance roundrobin se establece explícitamente porque las versiones más nuevas de HAProxy cambiaron el algoritmo de backend predeterminado a random, y roundrobin es más fácil de enseñar de manera predecible en un primer paso.
Aquí está el desglose en inglés simple de cada sección:
| Sección | Líneas clave | Qué hace |
|---|---|---|
| global | log stdout format raw local0 | Envía registros a stdout para que el registro de Docker sea directo. |
| defaults | mode http, timeouts | Establece el comportamiento HTTP de línea base y valores de tiempo de espera sensatos. |
| frontend http | bind :80, default_backend demo_backend | Crea el listener público y lo conecta a la definición del backend. |
| backend demo_backend | balance roundrobin, server demo1 demo:5678 check | Le dice a HAProxy qué servicio usar y monitorear su salud. |
| frontend stats | bind :8404, stats enable, stats uri /stats | Añade una página de validación local opcional para que puedas ver el estado en tiempo de ejecución más tarde. |
Puedes notar una cosa que falta: option forwardfor. Esta omisión es intencional en la ruta base. Preservar la IP del cliente original es útil más adelante, pero este primer despliegue se trata de probar el enrutamiento y la salud del backend, no de enseñar el comportamiento del encabezado con un contenedor de demostración que no hace que esa señal sea especialmente valiosa.
💡 Consejo: Siempre valida la configuración de HAProxy antes de iniciar la pila completa. Debido a que esta configuración se refiere al backend por su nombre de servicio de Compose (demo), inicia ese backend primero para que HAProxy pueda resolverlo durante la validación.
Ejecuta la validación desde el mismo directorio del proyecto:
docker compose up -d demo
docker compose run --rm --no-deps haproxy haproxy -V -c -f /usr/local/etc/haproxy/haproxy.cfg
Si el segundo comando termina con Configuration file is valid, ya has probado que HAProxy puede analizar el archivo correctamente y resolver el destino del backend antes de que cualquier listener en vivo se inicie.
Inicia la Stack y Prueba que el Proxy Funciona
Una vez que la configuración se valida, inicia la stack en modo detached:
Debido a que el paso de validación ya inició demo, este comando principalmente levanta HAProxy y reconcilia la stack completa de dos servicios:
docker compose up -d
Luego verifica que ambos contenedores estén activos:
docker compose ps
Esa vista de procesos es solo el primer punto de control. Confirma que Docker inició los contenedores, pero aún no que HAProxy esté enrutando exitosamente el tráfico al backend. La siguiente solicitud verifica la ruta de datos real.
Ahora ejecuta la prueba de enrutamiento real desde el VPS mismo:
curl -i http://127.0.0.1
La señal de éxito es HTTP/1.1 200 OK más el cuerpo de la respuesta que contiene Hello from the HAProxy demo backend. Algunas compilaciones de http-echo envuelven ese texto en una pequeña respuesta HTML, así que enfócate en la frase del cuerpo más que en el formato exacto.
Si quieres una prueba a nivel de navegador, abre http://YOUR_SERVER_IP desde otra máquina.

Para una segunda superficie de validación, verifica la página de estadísticas solo local desde el VPS:
curl http://127.0.0.1:8404/statsEn la página de estadísticas, las señales más útiles son un frontend llamado http, un backend llamado demo_backend, una fila de servidor llamada demo1, estado mostrado como UP, y generalmente un valor de último chequeo como L4OK in 0ms. También ten en mente un pequeño matiz de Docker: depends_on de sintaxis corta controla el orden de inicio, pero no espera a que un servicio sea saludable. Si el primer curl falla una vez justo después del inicio, espera unos segundos e intenta de nuevo antes de asumir que la configuración es incorrecta.
La diferencia entre estado de proceso y éxito real es más fácil de mantener clara en forma de tabla:
| Estado | Qué te dice |
|---|---|
| Los contenedores están ejecutándose | Docker inició los procesos. |
| curl -i http://127.0.0.1 devuelve 200 OK y la frase de demostración | HAProxy está enrutando realmente el tráfico al backend. |
| La página de estadísticas muestra demo1 como UP | HAProxy ve el backend como saludable. |
Errores comunes en la primera ejecución y soluciones rápidas

Si la configuración no funciona inmediatamente, resiste la tentación de reescribir ambos archivos a la vez. La mayoría de los fallos en la primera ejecución en este stack son predecibles, y se vuelven mucho más fáciles de corregir cuando cambias una variable a la vez.
Usa esta matriz como capa de diagnóstico rápido:
HAProxy se cierra inmediatamente
Causa probable: haproxy.cfg no existe.
Solución rápida: Asegúrate de que haproxy.cfg existe junto a compose.yaml.
Por qué sucede: La imagen oficial no incluye una configuración lista para usar.
El error dice que no puede abrir /usr/local/etc/haproxy/haproxy.cfg
Causa probable: Ruta de bind-mount incorrecta.
Solución rápida: Verifica que ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro sea exacto.
Por qué sucede: HAProxy no puede iniciarse sin un archivo de configuración válido.
El error dice Permiso denegado en el puerto 80
Causa probable: Problema de vinculación de puerto privilegiado.
Solución rápida: Mantén net.ipv4.ip_unprivileged_port_start: “0” en Compose, o mueve tanto HAProxy como el puerto publicado a 8080.
Por qué sucede: El contenedor se ejecuta como el usuario no root haproxy.
Cambiaste la asignación a 8080:80 y aún obtienes un error de vinculación
Causa probable: HAProxy aún se vincula a :80 dentro del contenedor.
Solución rápida: Cambia tanto la asignación del host como la línea bind interna si te alejas del puerto 80.
Por qué sucede: La regla de puerto privilegiado también se aplica dentro del contenedor.
El puerto 80 ya está en uso
Causa probable: Otro servicio controla el puerto del host.
Solución rápida: Vuelve a ejecutar la verificación ss y detén o mueve el servicio conflictivo.
Por qué sucede: Solo un proceso puede escuchar en el mismo puerto del host.
La verificación de sintaxis reporta palabra clave desconocida o errores específicos de línea
Causa probable: Error tipográfico en la configuración de HAProxy.
Solución rápida: Vuelve a ejecutar la verificación de sintaxis y corrige la línea exacta que reporta.
Por qué sucede: El analizador de HAProxy es estricto, lo cual es útil una vez que lo usas intencionalmente.
Los contenedores están activos, pero curl no devuelve la respuesta de demostración
Causa probable: Ruta de enrutamiento incorrecta.
Solución rápida: Vuelve a verificar default_backend demo_backend, server demo1 demo:5678 check, y el nombre del servicio demo.
Por qué sucede: Un contenedor en ejecución no es prueba de una ruta frontend-a-backend correcta.
curl local funciona pero el sitio es inaccesible desde el exterior
Causa probable: Firewall o regla de seguridad del proveedor.
Solución rápida: Abre el puerto 80 en UFW y en cualquier firewall del lado del proveedor.
Por qué sucede: La publicación local puede funcionar incluso cuando el acceso público aún está bloqueado.
⚠️ Advertencia: Cambia una cosa a la vez. Si editas compose.yaml y haproxy.cfg a ciegas, haces mucho más difícil determinar si el fallo es un problema de ruta de archivo, un problema de puerto o un problema de enrutamiento.
Cuando necesites evidencia rápida, mantén estos comandos a mano:
docker compose logs haproxy
docker compose ps
docker compose up -d demo
docker compose run --rm --no-deps haproxy haproxy -V -c -f /usr/local/etc/haproxy/haproxy.cfg
sudo ss -tlnp | grep -E ':(80|8404)s' || trueEstos son los tres patrones de alerta más dignos de reconocer a primera vista:
[ALERT] ... Cannot open configuration file /usr/local/etc/haproxy/haproxy.cfg : No such file or directory
[ALERT] ... Starting frontend http: cannot bind socket (Permission denied) [0.0.0.0:80]
[ALERT] ... parsing [/usr/local/etc/haproxy/haproxy.cfg:12] : unknown keyword 'chekc'; did you mean 'check' maybe?Esa es la parte tranquilizadora de un pequeño despliegue inicial: las formas de fallo suelen ser pequeñas también. No necesitas empezar de nuevo. Necesitas identificar qué capa se está quejando y corregir esa única cosa primero.
A Dónde Ir Después de la Instalación
Una vez que la demostración de un backend funciona, la arquitectura ya es útil. El siguiente paso real es reemplazar el contenedor de demostración con tu aplicación real manteniendo la misma estructura de HAProxy. Después de eso, agrega HTTPS/TLS como un paso de seguimiento dedicado, y trata el enrutamiento basado en dominio más ACLs como temas separados en lugar de apresurarte a incluirlos en esta primera instalación.

Cuando estés listo para más de un backend, el mismo patrón se vuelve visiblemente significativo:
backend app_backend
balance roundrobin
server app1 app1:8080 check
server app2 app2:8080 checkPara ediciones seguras de una configuración montada por bind, valida primero y luego recarga HAProxy de manera elegante:
docker compose up -d demo
docker compose run --rm --no-deps haproxy haproxy -V -c -f /usr/local/etc/haproxy/haproxy.cfg
docker compose kill -s HUP haproxy📝 Nota: La página de estadísticas es intencionalmente solo local en esta guía. Si alguna vez la expones públicamente, agrega autenticación y controles de acceso primero.
Eso te devuelve al problema original: querías una puerta frontal limpia frente a un servicio, sin convertir la primera configuración en un proyecto de operaciones completo. Ahora tienes ese camino funcionando. Lo más importante es que también tienes el modelo mental correcto: HAProxy recibe el tráfico primero, lo reenvía a donde corresponde, y te da una forma más limpia de hacer crecer la pila en un VPS autogestionado sin perder el control de la configuración.
en todos los servicios de hosting