Puertos y EXPOSE en Docker | Nicolás Garzón
EXPOSE
-p
Texto
Copiar cliente
→ host 127.0.0.1:8080
→ regla de publicación
→ container 172.x.x.x:3000
→ proceso escuchando en 0.0.0.0:3000Estas son fronteras distintas. Un puerto puede estar documentado pero no publicado; publicado pero sin proceso escuchando; o accesible desde otros containers sin aparecer en el host.
JavaScript
Copiar server. listen ( 3000 , '0.0.0.0' ) ; El 3000 es el puerto interno. 0.0.0.0 indica que escucha en todas las interfaces visibles dentro del namespace.
JavaScript
Copiar server. listen ( 3000 , '127.0.0.1' ) ; solo acepta conexiones desde el loopback del mismo contenedor. Otro container o una regla de publicación puede llegar a la interfaz, pero el proceso no está enlazado allí.
documenta un puerto esperado;
queda como metadata;
puede ser utilizado por tooling;
admite TCP o UDP.
abre firewall;
publica en el host;
obliga al proceso a escuchar;
garantiza que el puerto sea correcto;
protege el servicio.
docker
Copiar EXPOSE 3000/tcp
EXPOSE 8125/udpBash
Copiar docker run -p 8080 :3000 my-apiTexto
Copiar [host_ip:]host_port:container_port[/protocol]Bash
Copiar docker run -p 127.0 .0.1:8080:3000/tcp my-api
docker run -p 0.0 .0.0:8080:3000 my-api
docker run -p '[::1]:8080:3000' my-apiLa sintaxis y soporte IPv6 dependen de plataforma y red. Verifica el resultado, no asumas equivalencia entre Linux y Docker Desktop.
Bash
Copiar docker run -p 127.0 .0.1:5432:5432 postgres:17El puerto queda disponible para procesos del host, pero no se publica intencionalmente en interfaces externas. Sigue necesitando autenticación.
Bash
Copiar docker run -p 5432 :5432 postgres:17Puede exponer PostgreSQL a la red. El alcance final depende de firewall, routing y plataforma.
Para una aplicación detrás de Nginx o Caddy:
Texto
Copiar internet
→ reverse proxy :443
→ API publicada solo en 127.0.0.1:3000No publiques directamente cada servicio si el proxy es el único punto de entrada.
Containers en la misma network no necesitan -p para comunicarse.
Bash
Copiar docker network create backend
docker run -d --network backend --name db postgres:17
docker run -d --network backend --name api my-apiPublicar 5432 sería necesario solo si el host o un sistema externo debe acceder.
Texto
Copiar localhost:5432significa “la API misma”, no el container de PostgreSQL ni el host.
Para otro container usa el nombre DNS del servicio. Para alcanzar el host, utiliza el mecanismo soportado por la plataforma y diseña esa dependencia explícitamente.
Bash
Copiar docker run -P my-api
docker port < container> -P publica los puertos expuestos en puertos disponibles del host.
pruebas paralelas;
entornos efímeros;
evitar colisiones.
La aplicación de prueba debe descubrir el puerto asignado, no asumir uno fijo.
Bash
Copiar docker run -p 127.0 .0.1::3000 my-apipuede solicitar un host port dinámico según soporte de la CLI.
Publicar TCP no publica UDP:
Bash
Copiar docker run \
-p 8080 :8080/tcp \
-p 8080 :8080/udp \
my-serviceProtocolos y healthchecks deben coincidir con el servicio real.
La implementación puede usar reglas de NAT, proxies de usuario u otros mecanismos según sistema, versión y configuración.
Texto
Copiar packet llega al host
→ regla de forwarding
→ interface del container
→ socket del procesoNo dependas de manipular manualmente reglas internas creadas por Docker. Integra firewall y políticas de red mediante mecanismos soportados.
En macOS y Windows, los containers Linux viven en una VM. Docker Desktop reenvía puertos desde el host hacia esa VM y el container.
la IP interna del container no suele ser una interfaz accesible directamente desde macOS;
publicación y host networking pueden comportarse diferente a Linux;
VPN y firewalls pueden interferir en otra frontera;
localhost del host puede ser reenviado por Desktop.
Prueba en la plataforma objetivo.
YAML
Copiar services :
api :
image : my- api
ports :
- "127.0.0.1:3000:3000"
db :
image : postgres: 17
expose :
- "5432" En Compose, expose documenta/acota puertos internos para el modelo, pero no publica al host. La network del proyecto ya permite comunicación entre servicios autorizados.
Publicar un puerto aumenta superficie:
scanners pueden encontrarlo;
el servicio puede recibir tráfico sin pasar por proxy o WAF;
una database puede quedar accesible;
interfaces administrativas pueden exponerse;
protocolos sin TLS pueden salir de la frontera esperada.
publicar solo puntos de entrada;
bind explícito a 127.0.0.1 cuando corresponda;
firewall;
autenticación y TLS;
networks internas;
no confiar en que un puerto “difícil” sea secreto;
revisar exposición real con herramientas del host.
Texto
Copiar internet
→ proxy container :443 publicado
→ api:3000 interno
→ db:5432 internoYAML
Copiar services :
proxy :
image : caddy
ports :
- "80:80"
- "443:443"
networks : [ public, backend]
api :
image : my- api
networks : [ backend, data]
db :
image : postgres: 17
networks : [ data]
networks :
public :
backend :
internal : true
data :
internal : true Solo el proxy publica. La API y database usan DNS interno.
Bash
Copiar docker ps -a
docker logs apiDesde la app o una herramienta disponible:
Bash
Copiar docker exec api sh -c 'ss -lntp || netstat -lntp' Una image mínima puede no incluir estas herramientas. Usa debug container, logs o inspección del host.
127.0.0.1:3000 dentro del container no es igual a 0.0.0.0:3000.
Bash
Copiar docker port api
docker inspect api --format '{{json .NetworkSettings.Ports}}' Bash
Copiar curl -v http://127.0.0.1:8080/healthBash
Copiar docker run --rm --network backend curlimages/curl \
-v http://api:3000/healthDistingue DNS, timeout, connection refused y respuesta HTTP inválida.
La ruta llegó a un host/interfaz, pero no existe un proceso escuchando o la conexión fue rechazada.
Puede indicar firewall, routing, proceso bloqueado o destino inaccesible.
El nombre no se resolvió; revisa network compartida y aliases.
Revisa publicación, interfaz host y firewall.
Revisa DNS, network compartida y puerto interno. Los containers no deben conectarse al host port para hablar entre sí salvo diseño intencional.
Dos containers no pueden publicar normalmente el mismo IP:puerto simultáneamente.
Dockerfile expone 3000, pero PORT=4000. La metadata queda desalineada.
Revisa binding del proceso, daemon, network, DNS y firewall para ambas familias.
En plataformas compatibles, --network host comparte namespace de red y cambia el modelo de publicación. Reduce aislamiento y tiene diferencias de plataforma. No lo uses solo para evitar comprender -p.
UDP no tiene conexión equivalente a TCP; las herramientas y healthchecks deben ser apropiados.
Corrección: publicar con runtime cuando el host necesita acceso.
Corrección: nombre DNS en network compartida.
Corrección: network interna y acceso administrativo controlado.
Corrección: declarar alcance de exposición explícitamente.
Corrección: probar cada frontera: dentro, entre containers, host y exterior.
El proceso, EXPOSE y -p cumplen funciones distintas.
0.0.0.0 dentro del container permite escuchar en sus interfaces.
Containers de la misma network no necesitan publicación al host.
localhost siempre depende del namespace desde el que se usa.
Publicar un puerto es una decisión de seguridad.
Diagnostica por fronteras y tipos de error.
Comprueba lo aprendido
Explica por qué EXPOSE 3000 no hace accesible una API desde el host.
¿Qué diferencia existe entre 127.0.0.1:8080:3000 y 8080:3000?
Diseña la red de proxy, API y database sin publicar servicios internos.
¿Por qué una app que escucha en 127.0.0.1 falla aunque exista -p?
Construye un flujo de diagnóstico para connection refused.
Volumes y persistencia , donde el estado se separa del lifecycle del container y aparecen responsabilidades de ownership, backup y restore.