¿El contenedor no llega a la API solo por VPN? Haz un proxy en el host
El túnel vive en el host, así que el contenedor no tiene ruta a la dirección privada. Publica un proxy inverso estrecho y llámalo como host.docker.internal.
Tu servicio de Compose agota el tiempo contra una API que el navegador de la misma máquina abre sin queja alguna. Esa diferencia es la pista: la petición que funciona sale de un host con un túnel VPN, y la que se cuelga sale de un contenedor con su propia pila de red. Nada dentro del contenedor conoce las rutas del túnel, así que el destino privado sigue inalcanzable se escriba como se escriba el nombre. La solución es un salto deliberado a través del host, no una tabla de rutas más astuta.
Empieza por por qué difieren los dos caminos y luego decide dónde debe vivir el túnel.
Por qué el host funciona y el contenedor no
Levantar el túnel da al host una interfaz, un conjunto de rutas y, normalmente, una entrada de resolutor que aporta la VPN. Un contenedor no hereda nada de eso: arranca con una interfaz en el puente del motor y una ruta por defecto que apunta a ese puente. Las rutas instaladas en el host son invisibles para todo lo que se ejecuta dentro de ese espacio de nombres.
Por eso los síntomas parecen incoherentes y no simplemente rotos. El host resuelve el nombre privado y conecta, porque allí existen tanto el nombre como la ruta. El contenedor envía su paquete a una dirección privada que nada del camino del puente puede alcanzar, y la petición caduca. Añadir el nombre al archivo hosts del contenedor no cambia nada, porque la dirección misma es inalcanzable desde donde está el contenedor.
Llega al host desde dentro del contenedor
Un proceso dentro del contenedor que marca localhost está marcando el contenedor. Docker Desktop publica host.docker.internal como un nombre que apunta de vuelta a la máquina que ejecuta el motor; los motores de Linux lo dejan indefinido salvo que el servicio lo pida, que es lo que hace la asignación de abajo. Con eso, la aplicación apunta a un nombre y un puerto en lugar de la dirección privada.
services:
app:
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
INTERNAL_API_HOST: "host.docker.internal:8666"El túnel se queda en el lado del host del salto, y el puerto de esa variable es el que usará el proxy para escuchar. Un contenedor que pierde el túnel ahora falla en un único salto conocido en vez de intentar una ruta que nunca tuvo.
Elige dónde vive el túnel
Hay tres formas posibles para un servicio de Compose que necesita una API privada, y difieren en qué proceso posee el túnel. Un cliente VPN puede ejecutarse dentro de la imagen del contenedor, lo que exige exponer un dispositivo de túnel y conceder la capacidad de administración de red que lo acompaña. La aplicación puede compartir el espacio de nombres de red de un contenedor de túnel aparte, de modo que ambos procesos ven una pila y una tabla de rutas. O el túnel puede quedarse en el host, con un reenviador estrecho del host al contenedor.
Las dos primeras arrastran archivos de perfil, comportamiento de reconexión y política de resolución al proyecto, y hacen que cada servicio del archivo dependa de la salud del túnel. Es demasiado acoplamiento para una sola llamada a la API. Para una pila de desarrollo que lee un único punto final interno, el reenviador del lado del host es el cambio más pequeño que aún mantiene las credenciales donde las gestiona el equipo de plataforma.
Publica un único camino estrecho en el host
Empieza por lo que hereda el reenviador: ejecútalo con la cuenta que posee el túnel y sus sockets de salida tomarán las rutas del host y el resolutor del host. Un bloque location de nginx cubre un camino.
server {
listen 8666;
location /search/ {
proxy_pass http://10.20.30.40:8090/;
proxy_set_header Host internal-api.example;
proxy_read_timeout 60s;
}
}Trata la dirección y el puerto como sustitutos y pon el par en el que la API escucha de verdad. Cada upstream adicional merece su propio bloque location, y cada bloque debería admitir solo los caminos que llama este servicio, para que el proxy no se convierta en una puerta general a la red privada. Ata el oyente a la dirección del puente para los clientes que llegan por host-gateway, a loopback solo cuando el cliente comparte el espacio de nombres de red del host, y nunca a 0.0.0.0. Elige también un puerto que siga libre: un número ya ocupado deja al proxy sin escuchar mientras el contenedor sigue caducando, lo que no se distingue del fallo de red con el que empezaste.
Puertos, nombres y la decisión de TLS
La resolución de nombres se divide igual que el enrutamiento. Las consultas públicas del contenedor pasan por el resolutor del motor, así que un nombre privado que resuelve en el host no resolverá dentro del contenedor. El proxy es el componente que resuelve el nombre privado, porque hereda el resolutor del host.
TLS es la decisión que se pospone y luego explica el siguiente fallo. Si el proxy reenvía los bytes intactos, el contenedor ve el certificado propio de la API y necesita la CA interna en su almacén de confianza. Si el proxy termina TLS, esa confianza pasa al host y la aplicación empieza a construir enlaces desde la cabecera host que recibe. Elige un solo arreglo y aplícalo a cada recurso, porque una redirección emitida por el otro apunta a un nombre de host que el contenedor no puede resolver.
Verifica desde dentro del servicio
Envía la petición desde el contenedor, no desde el host: un curl sencillo contra el camino del proxy y luego la comprobación de salud de la propia aplicación. Cuando el proxy responde pero la aplicación sigue fallando, compara tres cosas: la URL base que construye, cualquier variable de proxy en su entorno y la CA en la que confía. Todas están en el entorno y los registros del contenedor, y arreglar cualquiera de ellas deja la VPN en paz.
Escribe el arreglo elegido en el README del proyecto mientras esté fresco. Si no, la próxima persona que tropiece con este tiempo agotado pasará la misma tarde redescubriendo que el túnel vive en el host.