ToolsOps

Docker Compose: Nginx como reverse proxy

Pon una app detrás de Nginx con Docker Compose: red interna, proxy_pass al nombre del servicio, healthcheck y cómo diagnosticar un 502 Bad Gateway. Incluye cuándo usar Traefik o Caddy en su lugar.

Un reverse proxy pone una sola puerta de entrada delante de una o varias apps: el cliente habla con Nginx y Nginx reenvía la petición al servicio correcto por la red interna. Esta guía monta lo mínimo para entenderlo (Nginx delante de una app) y dedica la mayor parte a lo que más cuesta: diagnosticar un 502.

compose.yaml: app detrás de Nginx

La app no publica puerto: solo lo expose a la red interna. Nginx es el único que publica un puerto al host, y atado a loopback.

services:
  app:
    build: .
    restart: unless-stopped
    expose:
      - "3000"
  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app
    healthcheck:
      test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost/ || exit 1"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 20s

Y el nginx.conf que montamos en conf.d/default.conf (un solo bloque server; la imagen ya trae el http que lo envuelve):

server {
    listen 80;

    location / {
        proxy_pass http://app:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

La clave: proxy_pass al nombre del servicio

proxy_pass http://app:3000; es lo que hace de proxy. app es el nombre del servicio en el compose, y 3000 el puerto interno donde escucha. No es localhost: dentro del contenedor de Nginx, localhost sería el propio Nginx. Las cabeceras X-Forwarded-* le dicen a tu app quién es el cliente original y si venía por HTTP o HTTPS.

Diagnosticar un 502 Bad Gateway

Un 502 significa que Nginx está vivo pero no consigue hablar con la app. Recórrelo por orden:

  • ¿El nombre y el puerto son correctos? proxy_pass debe apuntar al nombre de servicio exacto y al puerto interno (app:3000), no al puerto publicado en el host.
  • ¿La app está arriba? docker compose ps y docker compose logs app. Si se cayó o aún arranca, Nginx no tiene a quién reenviar.
  • ¿La app escucha en 0.0.0.0? Si tu app escucha en 127.0.0.1 dentro del contenedor, solo se acepta a sí misma; Nginx no puede entrar. Debe escuchar en 0.0.0.0 (todas las interfaces del contenedor).
  • Pruébalo desde dentro: docker compose exec nginx wget -qO- http://app:3000 te dice si Nginx alcanza la app por la red interna.

TLS y cuándo cambiar de herramienta

Aquí servimos HTTP plano: TLS (certificados, redirección a 443, renovación) queda fuera del alcance. Nginx a mano es transparente y estupendo para aprender o para un proxy sencillo. Si gestionas muchos servicios dinámicos o quieres HTTPS automático sin pelearte con la config, mira Caddy (TLS automático, configuración mínima) o Traefik(descubrimiento dinámico por labels). La elección depende de cuánta automatización necesites, no de que uno sea "mejor" en abstracto.

Siguientes pasos

Obtén el servicio Nginx de partida en el generador de Docker Compose y vuelve al índice de ejemplos para ver cómo encaja con tu app o tu base de datos.

Preguntas frecuentes

Me sale 502 Bad Gateway, ¿qué significa?
Que Nginx recibió tu petición pero no pudo hablar con el upstream (tu app). Las causas habituales: el nombre o el puerto en proxy_pass no coinciden con el servicio real (debe ser http://app:3000, el nombre del servicio y el puerto interno); la app aún no ha arrancado o se cayó; o la app escucha en 127.0.0.1 dentro de su contenedor en vez de en 0.0.0.0, así que no acepta conexiones desde Nginx. Revisa docker compose logs app y docker compose ps.
¿Por qué uso 'expose' en la app y 'ports' solo en Nginx?
Porque la app no necesita ser accesible desde el host: solo Nginx habla con ella, por la red interna. expose: 3000 documenta el puerto interno sin publicarlo. Nginx es el único que publica un puerto al host (127.0.0.1:8080:80). Así toda la entrada pasa por el proxy y no expones la app directamente.
¿Por qué proxy_pass apunta a 'app' y no a 'localhost'?
Porque dentro del contenedor de Nginx, localhost es el propio Nginx. La app es otro contenedor y se resuelve por su nombre de servicio en la red interna de Compose: http://app:3000. Es el mismo principio que conectar una app a postgres:5432 por nombre.
¿Esto incluye HTTPS/TLS?
No, y es a propósito. Esta guía cubre el reverse proxy en HTTP plano para desarrollo local. TLS añade certificados, redirecciones 80 a 443 y renovación, que quedan fuera del alcance aquí. Si necesitas HTTPS con poco esfuerzo, Caddy obtiene y renueva certificados automáticamente y Traefik los integra con Let's Encrypt.
¿Cuándo usar Traefik o Caddy en vez de Nginx a mano?
Nginx con un nginx.conf escrito a mano es transparente y perfecto para aprender o para un proxy sencillo. Si tienes muchos servicios que aparecen y desaparecen, o quieres TLS automático y enrutado por etiquetas/labels sin editar config, Traefik (descubrimiento dinámico) o Caddy (TLS automático, config mínima) suelen dar menos fricción. No hay una respuesta única: depende de cuánta automatización necesites.