Docker

Si tus servicios se ejecutan con Docker Compose, añade nuestro contenedor al proyecto y serán accesibles desde internet en tu dirección IPv4 pública dedicada, sin cambiar nada en los propios servicios. El tráfico entrante llega por el túnel y no a través de un puerto publicado, así que no tienes que añadir entradas ports:, no necesitas una dirección enrutable de tu operador y no hay nada que abrir en el router ni en el firewall del host.

Antes de empezar

  • Una dirección IP de GetPublicIP y su clave API. Consulta Crear una clave API
  • Linux: Docker Engine y Compose v2, es decir, docker compose en lugar del antiguo script docker-compose
  • macOS: Docker Desktop, en Intel o Apple Silicon
  • Windows: Docker Desktop en modo de contenedores Linux, con un kernel de WSL2 6.6. Compruébalo con wsl --version y ejecuta wsl --update si muestra una versión anterior, porque a los kernels anteriores a 6.6 les falta un módulo de netfilter que el túnel necesita

Todo lo que hay en esta página funciona sin cambios en Windows y macOS con Docker Desktop, sin instalar nada más que el propio Docker Desktop. Docker Desktop es gratuito para uso personal, educación, proyectos de código abierto no comerciales y pequeñas empresas con menos de 250 empleados y menos de 10 millones de USD de ingresos anuales; las organizaciones más grandes necesitan una suscripción de pago de Docker. Consulta macOS y Windows para ver los detalles de cada uno.

La imagen se publica para linux/amd64 y linux/arm64 en ghcr.io/getpublicip/getpublicip.

Pon tu clave API en un archivo .env

Crea un archivo .env junto a tu archivo de Compose. Consulta Crear una clave API para más información.

GETPUBLICIP_API_KEY=your-api-key-from-the-dashboard

Compose lee el archivo .env automáticamente, y por eso los archivos de Compose de abajo hacen referencia a ${GETPUBLICIP_API_KEY} en lugar de contener la clave. Añade .env a tu .gitignore.

Añádelo a tu archivo de Docker Compose

Añade este servicio junto a los contenedores que ya ejecutas:

services:
  getpublicip:
    image: ghcr.io/getpublicip/getpublicip:latest
    restart: unless-stopped
    environment:
      GETPUBLICIP_API_KEY: "${GETPUBLICIP_API_KEY}"
      GETPUBLICIP_MAPPINGS: "8080/tcp->web:80; 443/tcp->web:443"
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      net.ipv4.ip_forward: 1
      net.ipv4.conf.all.src_valid_mark: 1
      net.ipv6.conf.all.forwarding: 1
    networks:
      - appnet

Sustituye appnet por la red en la que ya están tus servicios, y web por los nombres de tus propios servicios. Todo lo demás puede quedarse como está.

Mantén restart: unless-stopped. Es la forma en que el agente se recupera de los fallos que no puede arreglar sobre la marcha, como se describe en Caídas de conexión.

Un ejemplo completo que funciona

Si todavía no tienes un proyecto de Compose, este archivo es una configuración completa: un servidor web en tu propia dirección IP pública, accesible desde cualquier lugar. Guárdalo como compose.yaml:

services:
  web:
    image: nginx:alpine
    restart: unless-stopped
    networks:
      - appnet

  getpublicip:
    image: ghcr.io/getpublicip/getpublicip:latest
    restart: unless-stopped
    environment:
      GETPUBLICIP_API_KEY: "${GETPUBLICIP_API_KEY}"
      GETPUBLICIP_MAPPINGS: "80/tcp->web:80"
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      net.ipv4.ip_forward: 1
      net.ipv4.conf.all.src_valid_mark: 1
      net.ipv6.conf.all.forwarding: 1
    networks:
      - appnet

networks:
  appnet:

Después, inícialo:

docker compose up -d

Espera entre 30 y 60 segundos y abre http://{YOUR IP ADDRESS} desde cualquier red, o desde tu teléfono con datos móviles. Si ves la página de bienvenida de nginx, una solicitud desde internet ha llegado a tu contenedor.

web no tiene ninguna entrada ports:. El agente llega a nginx a través de la red interna appnet, y los dos servicios se unen a ella porque el agente resuelve web por su nombre de servicio, algo que solo funciona en una red que compartan ambos. Si quitaras los bloques networks:, Compose pondría los dos en la red predeterminada del proyecto, y eso funciona igual de bien, pero darle nombre a la red deja visible el requisito.

Redirige tus puertos

GETPUBLICIP_MAPPINGS decide qué puertos de tu IP pública van a cada uno de tus contenedores. Separa las entradas con ; o con saltos de línea:

publicPort[/proto[,proto]][/stack[,stack]]->service[:internalPort]
ParteValor predeterminadoNotas
publicPortNingunoun solo puerto, o un rango como 60000-60099
prototcptcp o udp; sepáralos con comas para usar ambos
stackipv4ipv4 o ipv6; sepáralos con comas para doble pila
serviceNingunoel destino, que se resuelve en tiempo de ejecución: un nombre de servicio de Compose, cualquier nombre de host que se pueda resolver o una dirección IPv4 literal
internalPortigual que publicPortel puerto en el destino; para un rango, omítelo o repite el mismo rango
EjemploSignificado
8080/tcp->web:80El TCP 8080 de tu IP pública va a web en el puerto 80
443/tcp,udp->web:443TCP y UDP 443 van a web en el puerto 443
53/udp->dns:53UDP 53 va a dns en el puerto 53
9000/tcp/ipv4,ipv6->apidoble pila; el puerto interno es 9000 de forma predeterminada
60000-60099/udp->turnUDP del 60000 al 60099 va a turn en los mismos puertos

Los segmentos que siguen al puerto se reconocen por palabra clave, así que su orden no importa. 8080/tcp/ipv4 y 8080/ipv4/tcp significan lo mismo.

Refiérete a tus contenedores por su nombre de servicio, nunca por su dirección IP. Compose asigna las IP de los contenedores de forma dinámica, así que el agente resuelve los nombres por su cuenta y los vuelve a comprobar cada 30 segundos. Cuando un contenedor se reinicia con otra IP, el agente actualiza las reglas de reenvío.

Un destino también puede ser cualquier nombre de host que se resuelva o una dirección IPv4 literal, y las tres opciones se comportan igual. No se acepta una dirección IPv6 literal; usa en su lugar un nombre de host con un registro AAAA.

Rangos de puertos

Algunos servicios necesitan un bloque entero de puertos en lugar de uno solo: un servidor TURN para llamadas de voz y videollamadas, FTP en modo pasivo o cualquier cosa que transporte medios RTP. Escribe el puerto público como un rango y cada puerto del rango se redirige al mismo puerto de tu contenedor:

GETPUBLICIP_MAPPINGS: "3478/tcp,udp->turn; 60000-60099/udp->turn"

El puerto 60042 de tu IP pública siempre llega al puerto 60042 de turn. Estos servicios le indican al otro extremo qué puerto usar, así que un puerto que hubiera cambiado por el camino rompería la conexión sin ningún error.

Por el mismo motivo, un rango solo puede ir al mismo rango. Puedes omitir el puerto interno, como arriba, o repetir el rango exactamente (60000-60099/udp->turn:60000-60099). Enviarlo a puertos distintos, como ->turn:61000-61099, se rechaza al iniciar el contenedor con a port range must forward to the same range. Un rango tampoco puede incluir el puerto 51820, que usa el propio túnel.

Abre solo los puertos que tu servicio esté configurado para usar. En un servidor TURN, eso significa el rango de puertos de relay que tenga configurado, no todo lo que está por encima de 49152. Requiere la imagen 1.3.1 o posterior.

Redirigir IPv6

Las redirecciones son IPv4 salvo que indiques otra cosa, así que añade la pila ipv6 para redirigir tu dirección IPv6 pública, o ipv4,ipv6 para redirigir ambas. Para que funcione tienen que cumplirse dos condiciones.

Tu red de Compose necesita tener IPv6 activado. Las redes bridge de Docker son solo IPv4 de forma predeterminada, así que un contenedor en una de ellas no tiene una dirección IPv6 a la que el agente pueda reenviar. Añade a la red enable_ipv6: true y una subred IPv6:

networks:
  appnet:
    enable_ipv6: true
    ipam:
      config:
        - subnet: 172.29.7.0/24
        - subnet: fd00:dead:beef::/64

La pila se aplica tanto al destino como al puerto público. Las reglas de reenvío no pueden traducir entre las dos familias de direcciones, así que una solicitud IPv6 tiene que llegar a tu contenedor por IPv6. Por eso, 8080/tcp/ipv4,ipv6->web:80 necesita que web tenga tanto una dirección IPv4 como una IPv6, y eso es lo que le da la red de arriba.

Mantén net.ipv6.conf.all.forwarding: 1 en el bloque sysctls del agente. Ya está en los ejemplos de esta página, y solo hace falta cuando rediriges IPv6.

Si falta algo, el agente te indica qué destino y qué familia fallaron, con no destination resolved in the family its mapping asked for. Si una configuración que por lo demás funciona bien muestra esa línea para una redirección ipv6, casi siempre se trata de una red sin enable_ipv6.

SSL / TLS

GetPublicIP trabaja a nivel de paquete. Reenvía TCP y UDP a tus contenedores, y el cifrado y los certificados quedan en tus manos: si sirves HTTPS, es tu propio servicio el que termina la conexión TLS. Redirige el 443 al contenedor que ya tiene tu certificado, y tu configuración TLS se queda como está:

GETPUBLICIP_MAPPINGS: "443/tcp->web:443; 80/tcp->web:80"

No desciframos tu tráfico

Tu IP pública no es un proxy TLS. No tiene ninguno de tus certificados ni de tus claves privadas, así que no tiene nada con lo que descifrar. La sesión TLS se negocia directamente entre el navegador de tu visitante y tu propio servicio, y permanece cifrada durante todo el trayecto: al salir del navegador, a través de internet hasta tu IP pública, por el túnel WireGuard y dentro de tu contenedor, donde se descifra por primera vez.

El túnel añade una segunda capa de cifrado, exterior, entre nuestra red y tu máquina, y dentro de ella viaja el mismo texto cifrado TLS que envió tu visitante. Tus certificados y claves privadas nunca salen de tu máquina, y tu tráfico nunca se descifra y se vuelve a cifrar por el camino. No desciframos el tráfico TLS ni tenemos planes de hacerlo.

Tu servicio necesita un certificado válido

Como el certificado está en el servicio que termina la conexión, ese servicio necesita un certificado válido, igual que en cualquier otro host público.

  • Apunta un dominio a tu IP pública. Añade un registro A para tu dirección IPv4, y un registro AAAA si también sirves IPv6. Las autoridades de certificación emiten certificados para nombres de dominio, así que una dirección IP sola no basta.
  • Obtén el certificado de Let's Encrypt. Es gratuito y automatizado. Caddy y Traefik lo solicitan y lo renuevan por ti sin más configuración que tu nombre de dominio; nginx y Apache se combinan con certbot.
  • Si usas un desafío HTTP-01, redirige el puerto 80 además del 443 al mismo servicio. Let's Encrypt se conecta al puerto 80 de tu IP pública para validar el desafío, y falla si solo está redirigido el 443. Un desafío DNS-01 no necesita el puerto 80.
  • No quites esas redirecciones. La renovación repite el mismo desafío aproximadamente cada 60 días, y falla sin avisar si la redirección ha desaparecido.

El HTTP sin cifrar sigue sin cifrar

Una redirección al puerto 80 que sirve HTTP sin cifrar va sin cifrar entre tu visitante y tu IP pública, exactamente igual que en cualquier otro lugar de internet. El túnel WireGuard solo cubre el tramo entre nuestra red y tu máquina. Si el contenido importa, activa TLS.

Redirigir a un servicio del host

Para hacer público un servicio que se ejecuta directamente en el host de Docker, apunta una redirección a host.docker.internal:

services:
  getpublicip:
    environment:
      GETPUBLICIP_MAPPINGS: "8081/tcp->host.docker.internal:8081"
    extra_hosts:
      - "host.docker.internal:host-gateway"   # needed on Linux; built in on Docker Desktop
  • El servicio del host tiene que escuchar en 0.0.0.0, no en 127.0.0.1. Un servicio vinculado a loopback rechaza la conexión que viene del contenedor. Compruébalo con ss -lntp.
  • Un firewall en el host la descartará sin avisar a menos que permitas el tráfico entrante desde la subred del bridge en docker0 o en la interfaz br-*.

Los destinos en el host y en contenedores se pueden mezclar libremente en un mismo GETPUBLICIP_MAPPINGS, salvo con Ver la IP real del cliente, que no funciona con destinos en el host.

Redes de Docker Compose

Como los destinos se resuelven por nombre de servicio, el contenedor getpublicip tiene que estar en la misma red de Docker que los contenedores a los que reenvía.

  • Una red predeterminada. Funciona tal cual.
  • Varias redes. Incluye getpublicip en cada red que contenga un servicio al que reenvíe.
  • Destinos en otro proyecto de Compose. Ambos proyectos tienen que unirse a una red externa compartida:
    docker network create shared
    

    Después, en cada archivo de Compose:
    services:
      getpublicip:
        networks:
          - shared
    
    networks:
      shared:
        external: true
    

Si esto está mal, los logs repiten waiting for destinations to resolve y el agente sigue reintentándolo en lugar de fallar directamente.

Revisa los logs

docker compose up -d
docker compose logs -f getpublicip

Un arranque correcto levanta el túnel en la interfaz pi0, resuelve tus destinos, instala las reglas de reenvío y envía las redirecciones a GetPublicIP. Después, el agente sigue en marcha y vuelve a comprobar los destinos y el propio túnel cada 30 segundos.

Un túnel en buen estado escribe en el log una línea como esta. Tenla presente para compararla si el túnel da problemas más adelante:

tunnel status up=true interfaces=1 peers=1 healthy=true handshakeAge=31s rxDelta=0

Fíjate en healthy=true. up=true solo significa que la interfaz existe, y sigue siendo true incluso en un túnel que ha dejado de pasar tráfico.

Pruebas

Espera unos 30 a 60 segundos antes de probar. Al enviar redirecciones nuevas, tu dirección IP pasa a un estado updating mientras las aplicamos, y las solicitudes que se hagan en ese intervalo fallarán. Es normal que haya dos o tres fallos seguidos de un acierto.

Haz la prueba desde una red que no sea la tuya. Lo más fácil es usar datos móviles:

curl http://{YOUR IP ADDRESS}

Caídas de conexión

El túnel se restablece solo tras una caída de la conexión, un reinicio del router o un cambio de dirección por parte de tu operador, sin que tengas que reiniciar ni configurar nada.

La mayoría de las veces se repara solo, sin que el agente tenga que hacer nada. El túnel es una conexión saliente que se mantiene abierta con un keepalive cada 25 segundos, y nuestro servidor vuelve a aprender tu dirección a partir de cualquier paquete que envíe el agente, así que, cuando tu router elimina la entrada NAT o te pasa a un puerto nuevo, el siguiente keepalive la restablece.

Para los fallos menos frecuentes que un keepalive no puede arreglar, el agente se detiene en lugar de intentar reparar el túnel sobre la marcha, y Docker lo reinicia con una conexión nueva. En el log verás tunnel appears broken cuando lo detecta por primera vez, y después exiting so the container is restarted si el problema persiste durante varias comprobaciones. Las dos líneas forman parte de la recuperación normal.

La recuperación depende de tu política de reinicio, así que mantén restart: unless-stopped en el servicio. Sin ella, el contenedor se detiene y se queda detenido.

Mientras el host esté sin conexión a internet, tus servicios tampoco serán accesibles. El túnel se recupera en cuanto vuelve a haber conexión.

Ver la IP real del cliente

De forma predeterminada, tus contenedores no ven la dirección IP del visitante. El contenedor de GetPublicIP reescribe la dirección de origen del tráfico entrante al reenviarlo, así que todos los visitantes aparecen en tus logs de acceso con la misma dirección IP local. La geolocalización, la limitación de solicitudes por cliente, el bloqueo de abusos y fail2ban ven todos una sola dirección IP.

GETPUBLICIP_PRESERVE_CLIENT_IP desactiva esa reescritura, y entonces tus contenedores ven la dirección IP real del cliente.

Requiere un cambio en cada servicio al que reenvías, así que lee esto antes de activarlo. Mientras la dirección de origen se reescribe con una de la red de Compose, tus servicios responden a través del contenedor de GetPublicIP automáticamente. Con GETPUBLICIP_PRESERVE_CLIENT_IP activado, la ruta predeterminada de cada servicio tiene que apuntar al contenedor de GetPublicIP, para que sus respuestas vuelvan al cliente a través de tu dirección IP pública. Sin esa ruta, las respuestas salen por tu conexión a internet normal y el cliente las descarta.

Requiere la imagen 1.1.0 o posterior.

El ajuste se aplica a todo el agente y no a una sola redirección, así que afecta a todos los servicios a los que reenvía ese agente.

Por qué hay que cambiar la ruta predeterminada

Todo lo demás de esta sección se deriva de lo que le ocurre a una solicitud que entra y a su respuesta, que vuelve a salir:

De forma predeterminada

visitor 203.0.113.9 → your public IP → tunnel → agent 
agent rewrites the source to its own address, 172.29.7.2
web replies to 172.29.7.2, which is on its own subnet
agent puts the real address back, reply leaves through the tunnel

Con la preservación de la IP del cliente activada

visitor 203.0.113.9 → your public IP → tunnel → agent
web sees 203.0.113.9 and then send replies to 203.0.113.9
the web container reply will follow the default route 

Un contenedor decide adónde enviar una respuesta mirando primero su propia subred y después su ruta predeterminada. Mientras se reescribe la dirección de origen, todas las respuestas van dirigidas a algo de la subred local, y la ruta predeterminada nunca entra en juego. Con la preservación de la IP del cliente activada, todas las respuestas van dirigidas a algún lugar de internet, así que la ruta predeterminada decide sobre todas ellas. La solución es, por tanto, una ruta configurada dentro de cada servicio, y ningún ajuste del agente puede sustituirla.

Docker no tiene una forma integrada de hacerlo, así que los ejemplos de abajo configuran la ruta predeterminada de cada contenedor de servicio con un pequeño contenedor sidecar.

Proyecto de ejemplo

Hay dos versiones del mismo proyecto. Empieza por la de solo IPv4, a menos que también redirijas tu dirección IPv6 pública. Cada línea de IPv6 de abajo es una cosa más que acertar, y una configuración IPv6 a medias te deja un servicio que funciona por IPv4 y se queda colgado por IPv6.

Solo IPv4

services:
  getpublicip:
    image: ghcr.io/getpublicip/getpublicip:latest
    restart: unless-stopped
    environment:
      GETPUBLICIP_API_KEY: "${GETPUBLICIP_API_KEY}"
      GETPUBLICIP_MAPPINGS: "80/tcp->web:80"
      GETPUBLICIP_PRESERVE_CLIENT_IP: "true"
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      net.ipv4.ip_forward: 1
      net.ipv4.conf.all.src_valid_mark: 1
    networks:
      appnet:
        ipv4_address: 172.29.7.2      # fixed, because the sidecar routes to it

  web:
    image: nginx:alpine
    restart: unless-stopped
    networks:
      appnet:
        ipv4_address: 172.29.7.3      # give every service a fixed address, not just the agent

  # Points web's default route at the agent, from inside web's own network
  # namespace. Runs once and exits.
  web-route:
    image: alpine:3.20
    network_mode: "service:web"
    cap_add:
      - NET_ADMIN
    restart: "no"
    command: sh -c 'ip route replace default via 172.29.7.2'
    depends_on:
      web:
        condition: service_started
        restart: true               # re-run this sidecar whenever web restarts
      getpublicip:
        condition: service_started

networks:
  appnet:
    driver: bridge
    ipam:
      config:
        - subnet: 172.29.7.0/24
          gateway: 172.29.7.1

IPv4 e IPv6

El mismo proyecto con la mitad de IPv6 añadida. Cambian cuatro cosas: la red recibe enable_ipv6: true y una subred IPv6, cada servicio recibe una ipv6_address fija, el agente recibe el sysctl net.ipv6.conf.all.forwarding y el sidecar configura una segunda ruta predeterminada.

services:
  getpublicip:
    image: ghcr.io/getpublicip/getpublicip:latest
    restart: unless-stopped
    environment:
      GETPUBLICIP_API_KEY: "${GETPUBLICIP_API_KEY}"
      GETPUBLICIP_MAPPINGS: "80/tcp/ipv4,ipv6->web:80"
      GETPUBLICIP_PRESERVE_CLIENT_IP: "true"
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      net.ipv4.ip_forward: 1
      net.ipv4.conf.all.src_valid_mark: 1
      net.ipv6.conf.all.forwarding: 1
    networks:
      appnet:
        ipv4_address: 172.29.7.2      # fixed, because the sidecar routes to it
        ipv6_address: fd00:c0de:7::2

  web:
    image: nginx:alpine
    restart: unless-stopped
    networks:
      appnet:
        ipv4_address: 172.29.7.3      # give every service a fixed address, not just the agent
        ipv6_address: fd00:c0de:7::3

  # Points web's default routes at the agent, from inside web's own network
  # namespace. Runs once and exits. Both families need their own route.
  web-route:
    image: alpine:3.20
    network_mode: "service:web"
    cap_add:
      - NET_ADMIN
    restart: "no"
    command: >
      sh -c 'ip route replace default via 172.29.7.2;
      ip -6 route replace default via fd00:c0de:7::2'
    depends_on:
      web:
        condition: service_started
        restart: true               # re-run this sidecar whenever web restarts
      getpublicip:
        condition: service_started

networks:
  appnet:
    driver: bridge
    enable_ipv6: true
    ipam:
      config:
        - subnet: 172.29.7.0/24
          gateway: 172.29.7.1
        - subnet: fd00:c0de:7::/64
          gateway: fd00:c0de:7::1

Sea cual sea la versión de la que partas, repite el sidecar para cada servicio al que reenvíes y comprueba los cuatro puntos siguientes. Si falla cualquiera de ellos, el servicio se queda colgado sin mostrar ningún error.

Da una dirección fija a cada servicio de la red, no solo al agente. Un servicio sin dirección fija toma la dirección libre más baja, que es la que reservaste para el agente, y por depends_on a menudo arranca primero y se la queda. Entonces el agente ni siquiera llega a arrancar y muestra Address already in use.

Si rediriges IPv6, configura las dos rutas. La ruta predeterminada de IPv6 es independiente de la de IPv4, así que un sidecar que solo configura la ruta IPv4 deja que las respuestas IPv6 salgan por el bridge, y esas conexiones se quedan colgadas mientras IPv4 funciona. La segunda línea ip -6 route replace del ejemplo de doble pila configura esa ruta. Olvidarla es el fallo más habitual de esa versión, porque todo lo que pruebas por IPv4 funciona.

La ruta no sobrevive a un reinicio de tu servicio. Docker crea un espacio de nombres de red nuevo con una tabla de rutas limpia, así que la ruta desaparece y el tráfico entrante se detiene hasta que el sidecar vuelve a ejecutarse. El depends_on de arriba se encarga de esto: restart: true le indica a Compose que vuelva a ejecutar el sidecar cada vez que web se reinicie, de modo que tanto docker compose restart web como docker compose up -d reparan la ruta por sí solos.

No cubre el caso en que Docker reinicia tu servicio por su cuenta. Si web se cae, o lo mata el out-of-memory killer, su propia política restart: lo vuelve a levantar sin que intervenga Compose, y nada vuelve a ejecutar el sidecar. Falta la ruta, el tráfico entrante se detiene y nada avisa de un problema. Restablécela con:

docker compose up -d web-route

Recomendamos añadir la ruta predeterminada en el Dockerfile del servicio para que sobreviva a estos reinicios. Si no, comprueba las rutas con docker exec web ip route y vuelve a configurar la ruta predeterminada cada vez que haya desaparecido.

Los destinos en el host no pueden funcionar en este modo. Reenviar a un servicio del host de Docker requiere también que la respuesta vuelva a través del agente, y eso implicaría cambiar la ruta predeterminada del propio host.

Comprueba que funciona

Haz una solicitud a tu dirección IP:

curl http://{YOUR IP ADDRESS}              # one request through your public IP
docker compose logs --tail 10 web           # what your container recorded

El log debería mostrar la dirección IP real del cliente. Si muestra 172.29.7.2, la preservación de la IP del cliente no está activada y estás viendo el comportamiento predeterminado.

Hacer la prueba desde el propio host de Docker no es problema. La solicitud sigue saliendo hacia tu IP pública, llega a nuestra red y vuelve por el túnel, así que tu contenedor registra la dirección pública de tu conexión a internet, fácil de distinguir de la dirección del bridge que habría puesto la reescritura.

Si rediriges IPv6, compruébalo por separado con curl -6. Las dos familias tienen rutas predeterminadas distintas, así que una puede funcionar mientras la otra está rota.

Cuando no funciona

Una ruta predeterminada mal configurada no produce ningún error. Las conexiones se quedan colgadas, el túnel sigue activo y sigue registrando healthy=true, las reglas de reenvío siguen siendo correctas y nada en docker compose logs getpublicip informa de un problema, porque desde el punto de vista del agente no lo hay. Las respuestas de tu contenedor salen por el camino equivocado en lugar de a través de tu dirección IP pública.

Revisa la tabla de rutas dentro del servicio:

docker exec web ip route      # default must be via the agent's address
docker exec web ip -6 route   # and this one too, if you forward IPv6

Enviar el tráfico saliente por tu IP

Las secciones anteriores tratan del tráfico entrante. GETPUBLICIP_OUTBOUND cubre la otra dirección: tus contenedores salen a internet desde tu IP pública en lugar de desde la dirección de tu operador. Es útil cuando algo a lo que te conectas solo permite una dirección conocida, o cuando quieres que tu tráfico saliente venga de la misma dirección que el entrante.

Necesita el mismo cambio de ruta predeterminada que la preservación de la IP del cliente, por el mismo motivo: la respuesta tiene que volver a través del agente. Si ya configuraste el sidecar para eso, solo tienes que añadir el ajuste.

services:
  getpublicip:
    image: ghcr.io/getpublicip/getpublicip:latest
    restart: unless-stopped
    environment:
      GETPUBLICIP_API_KEY: "${GETPUBLICIP_API_KEY}"
      GETPUBLICIP_MAPPINGS: "80/tcp->web:80"
      GETPUBLICIP_OUTBOUND: "true"
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun
    sysctls:
      net.ipv4.ip_forward: 1
      net.ipv4.conf.all.src_valid_mark: 1
      net.ipv6.conf.all.forwarding: 1
    networks:
      appnet:
        ipv4_address: 172.29.7.2

Después, apunta al agente cada contenedor que deba usarlo, exactamente igual que en el Proyecto de ejemplo de arriba. Un contenedor cuya ruta no toques sigue usando el bridge normal de Docker, así que puedes pasar los servicios de uno en uno.

Compruébalo desde dentro de un contenedor que hayas enrutado:

docker exec web curl -s https://api.getpublicip.com/ip

Debería mostrar tu dirección de GetPublicIP, no la de tu casa ni la de tu oficina.

Solo tráfico saliente, sin puertos redirigidos

Si lo único que quieres es la dirección saliente, GETPUBLICIP_MAPPINGS pasa a ser opcional:

    environment:
      GETPUBLICIP_API_KEY: "${GETPUBLICIP_API_KEY}"
      GETPUBLICIP_OUTBOUND: "true"

Un agente iniciado así deja las redirecciones de puertos de tu dirección IP como están, incluidas las que hayas configurado en el panel.

Lo que debes saber antes de activarlo

Tus contenedores comparten una dirección. Todo lo que hay detrás del agente sale desde la misma dirección IPv4 pública y la misma IPv6 pública, así que el otro extremo no puede distinguir un contenedor de otro.

Falla en modo cerrado. Un contenedor enrutado a través del agente no tiene internet en absoluto mientras el túnel esté caído, cuando antes habría usado tu conexión normal. Es intencionado, para que tu tráfico nunca vuelva a salir desde la dirección de tu operador sin que lo sepas, pero piénsalo antes de enrutar a través del agente algo de lo que dependes.

Si un contenedor no puede salir a internet, el problema casi siempre está en su ruta y no en el agente. Compruébalo dentro del propio contenedor:

docker exec web ip route      # default must be via the agent's address
docker exec web ip -6 route   # and this one too, if you use IPv6

Permisos del contenedor

AjustePor qué hace falta
cap_add: NET_ADMINConfigurar la interfaz WireGuard y las reglas de reenvío
devices: /dev/net/tunEl propio dispositivo del túnel
net.ipv4.ip_forward=1Reenviar el tráfico del túnel a tu contenedor de destino
net.ipv4.conf.all.src_valid_mark=1Lo requiere la configuración de rutas de wg-quick
net.ipv6.conf.all.forwarding=1Solo hace falta si rediriges IPv6

macOS

Usa Docker Desktop y el archivo de Compose de arriba funciona sin cambios, tanto en Intel como en Apple Silicon.

Otros entornos de Docker para macOS (Colima, OrbStack, Rancher Desktop) usan kernels distintos y no se han verificado, así que no podemos decir si el túnel se levanta en ellos.

Windows

El archivo de Compose de arriba no necesita cambios en Windows con Docker Desktop.

Docker Desktop necesita Windows 10 u 11 de 64 bits, en una versión que Microsoft siga admitiendo, y el backend WSL 2, que es el predeterminado. Después, hay dos cosas que comprobar antes de empezar.

1. Docker Desktop tiene que estar en modo de contenedores Linux. El motor de contenedores Windows no puede ejecutar esta imagen y, de todos modos, no está disponible con el backend WSL 2.

2. Tu kernel de WSL2 tiene que ser 6.6.x. A los kernels anteriores, entre ellos el 5.15, les falta un módulo de netfilter que el túnel necesita, y no hay forma de solucionarlo desde dentro del contenedor. El túnel no llega a levantarse y muestra unknown option '--save-mark'. Compruébalo y actualiza desde PowerShell:

wsl --version    # need "Kernel version: 6.6.x"
wsl --update     # if it reports anything older

La 6.6 no se consolidó como versión predeterminada hasta, aproximadamente, WSL 2.4 a 2.7, así que un equipo que lleve tiempo sin actualizarse puede seguir en la 5.15. El mismo requisito se aplica si ejecutas Docker CE dentro de una distribución de WSL2 en lugar de Docker Desktop, porque comparte el mismo kernel.

En la 6.6, tun es un módulo y no viene integrado en el kernel, así que comprueba que el nodo de dispositivo existe antes de buscar el problema en el agente. Este comando falla al crear el contenedor si falta:

docker run --rm --device /dev/net/tun alpine ls -l /dev/net/tun

No hace falta ninguna regla del Firewall de Windows ni reenviar ningún puerto. El tráfico entrante llega por el túnel, así que ni el Firewall de Windows ni el NAT de WSL están en el camino.

Portátiles y suspensión

La suspensión corta el túnel. En macOS y Windows, la máquina virtual Linux se suspende cuando lo hace tu equipo, y tu IP pública deja de ser accesible hasta que se reactiva; en ese momento el túnel se restablece automáticamente. Pruébalo en tu portátil si quieres, pero ejecútalo en un servidor: una máquina que se suspende es un mal host para una IP pública que tiene que estar siempre disponible.

Una clave API, un túnel

Una clave de WireGuard pertenece a un único extremo. Si dos contenedores se ejecutan con la misma clave API, se desconectarán el uno al otro. Gana el que haya hecho el handshake más reciente, y el otro descarta tráfico sin avisar hasta su siguiente keepalive. Parece una inestabilidad aleatoria en lugar de un error claro. La causa habitual es probar en un portátil mientras un servidor ya está conectado con la misma clave.

Este caso se diagnostica solo: los dos contenedores se reinician una y otra vez, porque cada uno concluye que el averiado es él. Un contenedor que se reinicia en bucle en una máquina cuya conexión a internet funciona bien casi siempre significa que un segundo agente está usando la misma clave.

Usa una clave API por cada túnel en funcionamiento.

Solución de problemas

docker compose logs -f getpublicip es el punto de partida.

Lo que vesQué significa
GETPUBLICIP_API_KEY is requiredLa variable no está definida, o no se está leyendo tu archivo .env
tunnel up: … 401La clave API no es válida
waiting for destinations to resolveEl destino todavía no está en marcha, el nombre del servicio es incorrecto o el agente no comparte ninguna red con él
Errores de wg-quick o de permisosFalta NET_ADMIN o /dev/net/tun
iptables-restore … unknown option '--save-mark'Solo en Windows: un kernel de WSL2 anterior a 6.6. Ejecuta wsl --update. Consulta Windows
error gathering device information … /dev/net/tunEl nodo de dispositivo no existe en el host. En Docker Desktop, comprueba la versión de tu kernel de WSL2
El tráfico llega, pero las respuestas se quedan colgadasLos sysctls de reenvío no están configurados
Se rechaza una redirección al hostEl servicio del host está vinculado a 127.0.0.1 en lugar de a 0.0.0.0, o un firewall del host está descartando la subred del bridge
tunnel appears brokenSe ha caído la conexión. El agente ya se está ocupando de ello. Consulta Caídas de conexión
tunnel health cannot be determinedEsta vez el agente no pudo leer el estado del túnel, lo cual no significa que el túnel esté caído. Se le da más tiempo para resolverse solo antes de reiniciar el contenedor
exiting so the container is restartedRecuperación normal, no un error. El contenedor vuelve con una conexión nueva. Si se repite durante mucho tiempo, o la máquina no tiene internet o dos agentes comparten una misma clave API
Nada en el log parece incorrecto, pero el tráfico entrante se queda colgado y GETPUBLICIP_PRESERVE_CLIENT_IP está activadoEl destino no envía sus respuestas de vuelta a través del agente. Ejecuta docker exec <target> ip route: la ruta predeterminada tiene que apuntar al agente, y también ip -6 route si rediriges IPv6. Consulta Ver la IP real del cliente
these destinations cannot work while preserving client IPsHay un destino en el host redirigido mientras la preservación de la IP del cliente está activada, y eso no puede funcionar. Pasa esas redirecciones a un segundo agente que deje el ajuste desactivado
a port range must forward to the same rangeUna redirección de rango apunta a puertos distintos en el destino, o hay un rango en un lado y un solo puerto en el otro. Omite el puerto interno o repite el mismo rango. Consulta Rangos de puertos
no destination resolved in the family its mapping asked forUna redirección ipv6 que apunta a un servicio sin dirección IPv6, normalmente una red de Compose sin enable_ipv6: true. Consulta Redirigir IPv6
Un contenedor que enrutaste a través del agente no tiene internet y GETPUBLICIP_OUTBOUND está activadoO el túnel está caído (este modo falla en modo cerrado a propósito) o la ruta predeterminada del contenedor no apunta al agente. Ejecuta docker exec <target> ip route, y también ip -6 route si usas IPv6. Consulta Enviar el tráfico saliente por tu IP
GETPUBLICIP_MAPPINGS is required (or set GETPUBLICIP_OUTBOUND=true for egress with no inbound ports)Iniciaste el agente sin redirecciones de puertos. Eso solo se permite en una configuración solo de salida, que necesita GETPUBLICIP_OUTBOUND=true
skipping API push: no mappings configured (egress-only)No es un error. Un agente solo de salida deja intactas las redirecciones de puertos de tu dirección IP en lugar de borrarlas
preserveClientIp and masqueradeOutbound are mutually exclusiveSe activaron a la vez GETPUBLICIP_PRESERVE_CLIENT_IP y GETPUBLICIP_MASQUERADE. Quita GETPUBLICIP_MASQUERADE
is not a boolean (use true or false)Un ajuste como GETPUBLICIP_PRESERVE_CLIENT_IP recibió un valor distinto de true o false. yes y on se rechazan a propósito, para que una errata no pueda dejar el ajuste desactivado sin que te des cuenta

Para comprobar una configuración sin levantar nada ni contactar con la API, ejecútala en modo de simulación (dry run). Muestra las reglas que aplicaría y termina:

docker run --rm \
  -e GETPUBLICIP_API_KEY=x -e GETPUBLICIP_DRY_RUN=true \
  -e GETPUBLICIP_ISOLATED_INTERFACE=eth0 \
  -e GETPUBLICIP_MAPPINGS="8080/tcp->web:80" \
  ghcr.io/getpublicip/getpublicip:latest

Preguntas frecuentes

¿Tengo que publicar puertos en mis contenedores para usar GetPublicIP?

No. El tráfico llega por el túnel WireGuard, no por un puerto publicado en el host, así que los servicios que expones no necesitan entradas ports: ni ningún otro cambio. Añadir ports: expondría también el servicio en tu red local, y normalmente eso no es lo que quieres.

¿El contenedor de GetPublicIP tiene que ejecutarse en modo privilegiado?

No. El contenedor necesita la capacidad NET_ADMIN, el dispositivo /dev/net/tun y los sysctls de reenvío, y nada más.

¿Tengo que instalar el módulo de kernel de WireGuard en el host?

No. La imagen usa el módulo de kernel del host cuando está disponible y, si no, recurre a wireguard-go, la implementación en espacio de usuario que incluye. No hay nada que instalar en el host en ningún caso, en ninguna plataforma.

¿Funciona en macOS y Windows?

Sí, y el archivo de Compose no necesita cambios en ninguno de los dos. El host de tu contenedor es una máquina virtual Linux y no el sistema operativo de escritorio, así que el dispositivo del túnel lo proporciona esa máquina virtual y no hay nada que instalar en macOS ni en Windows. macOS funciona sin cambios en Intel y Apple Silicon con Docker Desktop. En Windows, Docker Desktop tiene que estar en modo de contenedores Linux y tu kernel de WSL2 tiene que ser 6.6.x. Compruébalo con wsl --version y ejecuta wsl --update si muestra una versión anterior, porque a los kernels anteriores a 6.6 les falta un módulo de netfilter que el túnel necesita.

¿GetPublicIP descifra mi tráfico HTTPS?

No. GetPublicIP reenvía paquetes y nunca termina TLS, así que no tiene ninguno de tus certificados ni de tus claves privadas, ni nada con lo que descifrar. La sesión TLS se negocia directamente entre el navegador de tu visitante y el servicio tuyo que la termine, y se descifra por primera vez al llegar allí. Ese servicio necesita su propio certificado válido, igual que en cualquier otro host público. Let's Encrypt es gratuito y automatizado, y encaja bien aquí; si usas un desafío HTTP-01, recuerda redirigir el puerto 80 además del 443.

¿Puedo redirigir un puerto a un servicio que se ejecuta en el host de Docker en lugar de en un contenedor?

Sí. Apunta la redirección a host.docker.internal y, en Linux, añade extra_hosts: - "host.docker.internal:host-gateway". En Docker Desktop viene incluido. También funciona una dirección IP literal. Además, el servicio del host tiene que escuchar en 0.0.0.0 y no en 127.0.0.1, y un firewall en el host tiene que permitir el tráfico entrante desde la subred del bridge. Los destinos en el host son lo único que no se puede combinar con GETPUBLICIP_PRESERVE_CLIENT_IP.

¿Pueden mis contenedores ver la dirección IP real del visitante?

Sí, con GETPUBLICIP_PRESERVE_CLIENT_IP=true, y viene desactivado de forma predeterminada porque requiere trabajo por tu parte. Normalmente, el agente reescribe la dirección de origen del tráfico entrante con la suya propia para que las respuestas vuelvan a través de él, y por eso tus logs de acceso muestran la misma dirección para todos los visitantes. Desactivar esa reescritura significa que cada servicio al que reenvías tiene que enviar sus respuestas de vuelta a través del agente, lo que implica cambiar la ruta predeterminada de ese servicio. Se aplica a todo tu archivo de Compose y no a una sola redirección, y mientras está activado no se puede reenviar a un servicio del host de Docker. Configúralo siempre junto con la ruta, nunca por separado: la variable por sí sola te deja con un servicio que se queda colgado mientras los logs siguen pareciendo correctos.

¿Puedo usar X-Forwarded-For en lugar de GETPUBLICIP_PRESERVE_CLIENT_IP?

Por sí solo, no. X-Forwarded-For solo sirve si lo que añade la cabecera puede ver la dirección real del cliente desde el principio. La dirección de origen se reescribe a nivel de paquete, antes de que nada lea una solicitud HTTP, así que un proxy inverso dentro de tu archivo de Compose recibe paquetes que ya dicen venir del contenedor de GetPublicIP y no tiene nada verdadero que poner en la cabecera. Añadir uno ahí solo registra la dirección equivocada en un segundo lugar. Hay un caso en el que la cabecera sí funciona y no necesitas nada de esto: si tu tráfico pasa por una CDN o un proxy externo como Cloudflare antes de llegar a tu IP pública, ese servicio ve al cliente real y añade la cabecera él mismo, y tus contenedores deberían leerla de la forma habitual.

¿Pueden mis contenedores usar también mi IP pública para el tráfico saliente?

Sí, con GETPUBLICIP_OUTBOUND=true. De forma predeterminada, solo el tráfico entrante usa el túnel y tus contenedores siguen saliendo a internet por tu conexión normal. Al activarlo, su tráfico saliente también pasa por el túnel, de modo que sale desde tu dirección de GetPublicIP. Necesita el mismo cambio que ver la IP real del cliente (la ruta predeterminada de cada contenedor tiene que apuntar al contenedor de GetPublicIP), así que, si ya lo tienes configurado, solo te queda añadir el ajuste. Todos los contenedores que hay detrás comparten la misma dirección. Además, falla en modo cerrado a propósito: mientras el túnel está caído, esos contenedores no tienen internet en absoluto, en lugar de volver a la dirección de tu operador.

¿Qué pasa si se cae mi conexión a internet?

El túnel se restablece solo, y no hay nada que reiniciar ni que configurar. La mayoría de las caídas las repara el keepalive que mantiene abierta la conexión, sin que el agente tenga que intervenir; en un túnel real medimos que el tráfico volvía a fluir unos 17 segundos después de recuperarse la conectividad. Para los fallos menos frecuentes que un keepalive no puede arreglar, el agente detiene el contenedor en lugar de intentar reparar el túnel sobre la marcha, y tu política de reinicio lo vuelve a levantar enseguida con una conexión nueva; por eso el fragmento de Compose usa restart: unless-stopped. Mientras tu internet esté caído, tu servicio no es accesible, y el túnel se recupera en cuanto vuelve a haber conexión.

¿Qué pasa cuando uno de mis contenedores se reinicia con una dirección IP nueva?

El agente vuelve a resolver el nombre de servicio de cada destino a intervalos regulares (GETPUBLICIP_RESOLVE_INTERVAL, 30 segundos de forma predeterminada) y vuelve a aplicar las reglas de reenvío si un contenedor ha cambiado de dirección. No tienes que reiniciar nada. Es normal que se pierda una sola solicitud justo después de una resincronización, mientras se estabiliza el seguimiento de conexiones.

¿Puedo usar la misma clave API en dos lugares a la vez?

No. Una clave de WireGuard pertenece a un único extremo, así que dos contenedores que usan la misma clave API se desconectan el uno al otro. Gana el que haya hecho el handshake más reciente. Se manifiesta como fallos aleatorios e intermitentes en lugar de un error claro, aunque hay una señal inequívoca: los dos contenedores se reinician una y otra vez, porque cada uno concluye que el averiado es él. Usa una clave API por cada túnel en funcionamiento.


Primeros pasos explica cómo funciona el servicio. Para ejecutar tus servicios directamente en un host en lugar de en contenedores, consulta las guías de Linux y Windows.