Saltar al contenido

Capítulo 4 de 13

Conectar servicios sin exponerlo todo

Seguir una petición entre navegador, proxy y aplicación, usando DNS y redes de Docker sin memorizar direcciones IP.

#Primero, qué significa una dirección

Una conexión necesita encontrar un destino y un proceso que lo atienda. Una IP identifica una interfaz de red. Un puerto identifica el punto donde un programa recibe conexiones. Un nombre DNS permite encontrar una IP sin escribirla directamente. En http://api:8000/productos, http es el protocolo, api es el nombre, 8000 es el puerto y /productos es la ruta que interpretará la aplicación.

Estas piezas no se resuelven todas en Docker. Docker puede resolver api dentro de su red; después debe haber un proceso escuchando en 8000, y ese proceso debe saber responder a /productos. Un error de DNS, un rechazo de conexión y un HTTP 404 pertenecen a etapas distintas.

#Localhost siempre depende de quién habla

127.0.0.1 y localhost significan "esta misma máquina o espacio de red". Desde el navegador de tu portátil, apuntan al portátil. Dentro de un contenedor de API, apuntan a ese contenedor. Desde otro contenedor de base de datos, apuntan a la base de datos.

Si una API intenta conectar a localhost:5432, busca PostgreSQL dentro de sí misma. Si PostgreSQL está en el servicio db, la dirección correcta suele ser db:5432. No necesitas publicar el 5432 del host para que ambos hablen.

Recorrido navegador a puerto publicado, proxy a API mediante DNS, y API a base de datos por red interna.Ampliar diagrama ↗

Solo el proxy recibe tráfico publicado. API y base de datos usan sus puertos internos y los nombres de los servicios.

#Crear una red con nombres estables

docker network create red-practica
docker run -d --name web-interna --network red-practica nginx:stable-alpine
docker run --rm --network red-practica python:3.13-slim \
  python -c 'import urllib.request; print(urllib.request.urlopen("http://web-interna:80").status)'

Esperas 200. urllib.request es un cliente HTTP incluido en Python. El contenedor temporal actúa como cliente, resuelve web-interna y conecta al puerto 80. No hay -p: la comunicación ocurre dentro de una red creada por el usuario.

Las redes bridge creadas por el usuario tienen resolución de nombres entre sus contenedores. La red bridge predeterminada del motor tiene diferencias, incluido no proporcionar ese mismo descubrimiento por nombre; utiliza redes propias o las que crea Compose. No diseñes configuraciones alrededor de una IP que copiaste de inspect: cambia al recrear contenedores.

docker network inspect red-practica
docker run --rm --network red-practica python:3.13-slim \
  python -c 'import socket; print(socket.gethostbyname("web-interna"))'

El segundo comando comprueba solamente DNS. El primero muestra qué contenedores pertenecen a la red. Una respuesta DNS correcta no prueba que Nginx esté funcionando; por eso antes hicimos una petición HTTP completa.

#Publicar un puerto es atravesar una frontera

Hay tres decisiones separadas:

Decisión Ejemplo Qué permite
El proceso escucha 0.0.0.0:8000 dentro del contenedor Aceptar conexiones por sus interfaces
Los servicios comparten red api y db en datos Encontrarse y conectarse entre sí
El host publica un puerto 127.0.0.1:8080:8000 Entrar desde el host por 8080

EXPOSE 8000 en Dockerfile documenta el puerto. expose: [8000] en Compose tampoco establece una publicación al host ni una política de firewall que bloquee todos los demás puertos entre miembros de una red. Si el proceso escucha y hay conectividad, otro contenedor de esa red puede acceder.

Para una prueba local, limita la publicación a loopback: 127.0.0.1:8080:80. Para un servicio público, publicar 80:80 lo vincula a interfaces del host según la configuración de Docker y la pila de red. Esa amplitud debe ser intencionada.

En Linux, las reglas de publicación de Docker interactúan con el firewall. No supongas que "ufw bloquea ese puerto" significa que un puerto publicado de Docker queda bloqueado: el tráfico puede desviarse antes de atravesar esas reglas. Comprueba desde fuera y sigue las reglas de firewall de Docker para el backend utilizado. Lo más sencillo para una base privada es no publicar su puerto.

#Por qué separar red de entrada y red de datos

Una aplicación modesta puede usar una única red de Compose. Cuando añades un proxy, conviene dibujar quién necesita hablar con quién:

services:
  proxy:
    networks: [entrada]
  api:
    networks: [entrada, datos]
  db:
    networks: [datos]
networks:
  entrada:
  datos:
    internal: true

Este fragmento muestra solo la topología, no una aplicación completa. api conecta ambas partes a nivel de aplicación: recibe HTTP y hace consultas. No es necesario convertirla en un router de paquetes. db no comparte red con el proxy. internal: true crea una red externamente aislada; una API que además está en una red normal puede seguir teniendo salida por esa otra red. Por tanto, no es una garantía de que cualquier contenedor conectado a datos carezca de Internet.

La separación reduce conexiones accidentales, pero no es autenticación. Si la API puede hablar con la base, aún debe presentar credenciales con permisos limitados. Un programa comprometido dentro de la API conserva las capacidades que le diste.

#El navegador no vive dentro de la red de Docker

Una página JavaScript descargada desde un contenedor se ejecuta en el navegador del usuario. Si intenta fetch("http://api:8000"), ese nombre se resolverá en el ordenador del usuario, donde api normalmente no existe. Sirve la API a través del proxy con una ruta pública como /api o un dominio válido. Los nombres internos se usan en comunicaciones entre servidores.

Este error es frecuente porque el HTML lo sirve Docker, pero el lugar donde se ejecuta JavaScript es otro. Antes de elegir un hostname, pregunta siempre qué proceso hará la conexión.

#Diagnóstico siguiendo el recorrido

No cambies puertos, nombres y firewall al mismo tiempo. Comprueba una frontera por vez:

  1. Proceso: consulta estado y logs del destino. Si no arrancó, ninguna regla de red lo arregla.
  2. Escucha: verifica que escucha en la interfaz correcta. Escuchar en 127.0.0.1 dentro del contenedor solo permite conexiones desde ese contenedor.
  3. DNS: resuelve el nombre desde un contenedor de la red del cliente.
  4. Puerto: realiza una conexión al puerto interno esperado.
  5. Protocolo: pide la ruta real y observa HTTP, no solo que el socket abra.
  6. Publicación: repite desde host y después desde una máquina externa si el servicio es público.
Señal observada Hipótesis inicial Siguiente evidencia
Nombre no encontrado Servicio o red incorrectos Nombre de servicio y miembros de la red
Connection refused Host localizado, pero puerto sin escucha o rechazo explícito Proceso, puerto y dirección de escucha
Timeout Tráfico descartado, destino inaccesible o servicio bloqueado Firewall, rutas, carga y timeout configurado
HTTP 404 Llegaste a un servidor que no reconoce esa ruta Ruta y configuración del proxy
HTTP 502 desde proxy El proxy no logró una respuesta válida del upstream Logs del proxy y petición directa a la API

Si necesitas herramientas de diagnóstico, puedes ejecutar un contenedor temporal en la misma red, como el Python del ejemplo. Evita instalar un arsenal permanentemente en todas las imágenes solo para un incidente eventual.

#Casos en los que la receta cambia

Un servicio ejecutado directamente en el host no tiene automáticamente un nombre de servicio dentro de Compose. Docker Desktop ofrece host.docker.internal; en Linux puede requerir configuración explícita como host-gateway. Úsalo para una dependencia que realmente vive fuera de Docker, no para esquivar la configuración de redes entre contenedores.

network_mode: host comparte la red del host y altera el aislamiento; además las publicaciones de puertos dejan de tener el mismo papel. No es una solución general a problemas de DNS. Dos programas que intenten usar el mismo puerto del host chocarán.

Si un contenedor fue recreado, un cliente que guardaba su IP vieja debe reconectar y resolver otra vez el nombre. Los nombres estables permiten descubrir el nuevo destino, pero no mantienen vivas las conexiones TCP antiguas. Por eso las aplicaciones necesitan reconexión y límites de espera.

#Pequeña prueba con explicación

La API responde desde otro contenedor a http://api:8000, pero el navegador falla en esa misma URL. ¿Publicar 5432 de PostgreSQL tiene alguna relación?

Ver el razonamiento

No. La base de datos ni siquiera está en ese tramo. El navegador necesita un nombre y puerto accesibles desde el ordenador del usuario, normalmente el dominio del proxy. Después el proxy usará api:8000 dentro de Docker. Primero identifica dónde se origina cada conexión, después decide las direcciones.

Limpia solo esta práctica:

docker stop web-interna
docker rm web-interna
docker network rm red-practica

#Referencias para profundizar

    ↑ ↓ para recorrer · Intro para abrir · Ctrl K para buscar