Problema

En entornos de auto‑hosting es frecuente combinar wg‑easy (interfaz web para WireGuard) con Caddy como terminador TLS y reverse proxy. El objetivo es acceder a https://wg-easy.example.com sin exponer puertos internos. Un síntoma típico es el mensaje del navegador “Can’t connect to the server” o un error de conexión TCP. El problema no es exclusivo de wg‑easy; ocurre siempre que el proxy no logra resolver o alcanzar el contenedor objetivo dentro de la red Docker. La raíz suele estar en la configuración de la red Docker, la definición del reverse_proxy en el Caddyfile o en la exposición de puertos del servicio wg‑easy.

Causa

  1. Red Docker aislada – Si Caddy y wg‑easy no comparten la misma red, el nombre del contenedor (wg-easy) no se resuelve y Caddy devuelve una falla de conexión.
  2. Puertos internos no expuestos – wg‑easy escucha por defecto en el puerto 80 dentro del contenedor. Si el docker-compose.yml del servicio define PORT=80 pero no publica ese puerto (solo el UDP de WireGuard), Caddy necesita acceso interno, no externo; sin embargo, la directiva ports: debe incluir el puerto TCP si el contenedor no está en la misma red.
  3. Caddyfile mal estructurado – La sintaxis reverse_proxy wg-easy:80 requiere que el host sea resolvible dentro de la red de Caddy. Duplicar bloques, mezclar {} y tls internal sin separar correctamente puede generar que Caddy ignore la regla o caiga en un fallback que no sirve.
  4. TLS interno mal configurado – Usar tls internal sin habilitar el internal CA de Caddy o sin montar el directorio de datos (/data) puede provocar que Caddy no genere certificados y cierre la conexión antes de intentar el proxy.
  5. Conflicto de puertos en el host – Publicar 80:80 o 443:443 en varios contenedores genera colisiones y hace que el tráfico nunca llegue a Caddy.

Solución

1. Unificar la red Docker

Crea una red externa (por ejemplo caddy) y asegúrate de que ambos servicios la usen. En el docker-compose.yml de wg‑easy:

networks:
  caddy:
    external: true

En el docker-compose.yml de Caddy:

networks:
  caddy:
    name: caddy

Con esto, Caddy podrá resolver wg-easy mediante DNS interno de Docker.

2. Publicar solo los puertos necesarios

wg‑easy no necesita publicar su puerto HTTP al host; basta con que escuche internamente. Elimina cualquier línea - "80:80" bajo ports: del servicio wg‑easy. Mantén únicamente el UDP de WireGuard:

ports:
  - "51820:51820/udp"

3. Simplificar el Caddyfile

Un bloque limpio evita confusiones. Usa una única definición para el dominio y coloca la directiva reverse_proxy dentro del mismo bloque:

{
    email mymail@changed.com
    # Opcional: habilitar el CA interno
    # acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
}

wg-easy.example.com {
    reverse_proxy wg-easy:80
    tls internal
}

El bloque global {} define el correo y, si se desea, el CA. El bloque del dominio contiene solo reverse_proxy y tls internal. No es necesario envolverlo en otro par de llaves.

4. Montar los volúmenes de datos de Caddy

Para que tls internal funcione, Caddy necesita un directorio persistente donde guardar la autoridad certificadora. En el docker-compose.yml de Caddy:

volumes:
  - ./Caddyfile:/etc/caddy/Caddyfile:ro
  - caddy_data:/data
  - caddy_config:/config

Y declara los volúmenes al final del archivo:

volumes:
  caddy_data:
  caddy_config:

5. Reiniciar y validar la red

Después de ajustar los docker-compose.yml y el Caddyfile, ejecuta:

docker compose down
docker network create caddy   # solo si la red no existía
docker compose up -d

Esto recrea los contenedores con la nueva red y elimina posibles residuos de configuraciones anteriores.

Cuándo aplicar esta solución

  • Síntomas: Navegador muestra “Can’t connect to the server”, curl -v https://wg-easy.example.com termina en Connection refused o TLS handshake failure.
  • Entorno: Docker Compose con múltiples servicios, Caddy como único front‑end TLS, wg‑easy como backend sin exposición directa.
  • No aplica: Si se usa un balanceador externo (Traefik, Nginx) o si wg‑easy se ejecuta en modo host network y ya está accesible directamente en el puerto 80 del host.

Código

# docker-compose.yml (wg-easy)
services:
  wg-easy:
    image: ghcr.io/wg-easy/wg-easy:15
    container_name: wg-easy
    environment:
      - PORT=80
    volumes:
      - etc_wireguard:/etc/wireguard
      - /lib/modules:/lib/modules:ro
    ports:
      - "51820:51820/udp"
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    sysctls:
      - net.ipv4.ip_forward=1
      - net.ipv4.conf.all.src_valid_mark=1
      - net.ipv6.conf.all.disable_ipv6=0
      - net.ipv6.conf.all.forwarding=1
      - net.ipv6.conf.default.forwarding=1
    networks:
      - caddy

networks:
  caddy:
    external: true

volumes:
  etc_wireguard:
# docker-compose.yml (caddy)
services:
  caddy:
    image: caddy:2.10.0-alpine
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    restart: unless-stopped
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - caddy

networks:
  caddy:
    name: caddy

volumes:
  caddy_data:
  caddy_config:
# Caddyfile
{
    email mymail@changed.com
}

wg-easy.example.com {
    reverse_proxy wg-easy:80
    tls internal
}

Verificación

  1. Comprobar resolución DNS interna

    docker exec caddy ping -c 3 wg-easy
    

    Debería responder con la IP 10.42.42.42 (o la asignada).

  2. Probar el proxy con curl dentro del contenedor Caddy

    docker exec caddy curl -I http://wg-easy:80
    

    Debería devolver 200 OK y el encabezado Server: Caddy.

  3. Acceder desde el navegador
    Visita https://wg-easy.example.com. El certificado será emitido por la CA interna y la página de wg‑easy debe cargarse sin errores.

  4. Revisar logs

    docker logs caddy --follow
    

    Busca líneas que indiquen reverse_proxy exitoso y ausencia de dial tcp ...: connect: connection refused.

Notas adicionales

  • Persistencia de datos de wg‑easy: El volumen etc_wireguard debe estar fuera del contenedor para que las claves y configuraciones sobrevivan a reinicios.
  • Firewall del host: Asegúrate de que los puertos 80 y 443 estén abiertos en el firewall del servidor; Caddy solo necesita escuchar en ellos.
  • Modo “internal” vs Let’s Encrypt: tls internal es útil en entornos domésticos sin dominio público. Si el dominio es accesible desde internet, reemplaza tls internal por tls you@example.com para obtener certificados de Let’s Encrypt.
  • Depuración de red Docker: docker network inspect caddy muestra los contenedores conectados y sus IPs; útil para validar que ambos están en la misma subred.
  • Actualizaciones: Cuando actualices la imagen de wg‑easy, verifica que el puerto interno siga siendo 80; algunas versiones permiten cambiarlo mediante la variable PORT. Si lo cambias, actualiza también el reverse_proxy en el Caddyfile.