Programa
Al trabajar con contenedores Docker, uno de los aspectos más cruciales que he encontrado es habilitar la comunicación de red entre los contenedores y el mundo exterior. Sin una configuración adecuada de los puertos, las aplicaciones en contenedores permanecen aisladas e inaccesibles. En este artículo, te guiaré a través del proceso de exposición de puertos Docker, una habilidad fundamental para cualquiera que trabaje con aplicaciones en contenedores.
Esta guía está diseñada específicamente para programadores de software, ingenieros de DevOps y profesionales de TI que necesitan que sus aplicaciones en contenedores sean accesibles a los usuarios, a otros servicios o a sistemas externos. Tanto si estás desplegando un servidor web, una API o una base de datos dentro de un contenedor, comprender la exposición de puertos es esencial para construir sistemas funcionales basados en Docker.
Al final de este artículo, comprenderás claramente cómo funciona la red Docker, los distintos métodos de exposición de puertos y las técnicas prácticas para aplicar estos conceptos en tus propios proyectos.
Si eres nuevo en Docker, considera la posibilidad de seguir uno de nuestros cursos, como Introducción a Docker, Containerización y Virtualización con Docker y Kubernetes, Docker Intermedio o Conceptos de Containerización y Virtualización.
¿Qué es exponer un puerto en Docker?
Antes de entrar en los detalles técnicos, es importante entender qué significa la exposición de puertos en el ecosistema Docker.
Principios de aislamiento de la red de contenedores
Los contenedores Docker están diseñados con el aislamiento como principio básico. Este aislamiento se consigue mediante funciones del núcleo de Linux como los espacios de nombres y los cgroups. Los espacios de nombres proporcionan aislamiento de procesos, asegurando que los procesos de un contenedor no puedan ver ni interactuar con procesos de otros contenedores o del sistema anfitrión. Los Cgroups, en cambio, controlan la asignación de recursos, limitando cuánta CPU, memoria y otros recursos puede consumir un contenedor.
Desde el punto de vista de la red, cada contenedor recibe su propio espacio de nombres de red, completo con una interfaz Ethernet virtual (par veth). Esta interfaz se conecta por defecto a una red puente Docker, lo que permite la comunicación entre contenedores manteniendo el aislamiento de la red anfitriona. Piensa que cada contenedor tiene su propia dirección de red privada, invisible para el mundo exterior.
En su configuración por defecto, un contenedor Docker está completamente aislado. Los servicios que se ejecutan dentro pueden comunicarse entre sí, pero son inalcanzables desde fuera del contenedor, incluso desde la máquina anfitriona. Aquí es donde se hace necesaria la exposición portuaria.
Exponer vs publicar: distinción semántica
Cuando trabajes con la configuración de puertos Docker, te encontrarás con dos conceptos relacionados pero distintos: exponer y publicar puertos.
Exponer los puertos es principalmente una función de documentación. Cuando expones un puerto en Docker, esencialmente estás añadiendo metadatos a tu imagen de contenedor, indicando que la aplicación en contenedor escucha en puertos específicos.
Sin embargo, exponer un puerto no lo hace realmente accesible desde fuera del contenedor. Sirve de documentación para los usuarios de tu imagen.
Publicar puertos es lo que realmente hace que tus servicios en contenedores estén disponibles para el mundo exterior. La publicación crea un mapeo entre un puerto de la máquina anfitriona y un puerto dentro del contenedor. Cuando publicas un puerto, Docker configura la red del host para reenviar el tráfico del puerto del host especificado al puerto del contenedor correspondiente.
A continuación te indicamos cuándo utilizar cada enfoque:
- Utiliza exponer cuando construyas imágenes destinadas a ser utilizadas por otros usuarios de Docker, para documentar qué puertos utiliza tu aplicación.
- Utiliza publicar cuando necesites que sistemas externos (incluido el anfitrión) accedan a servicios que se ejecutan dentro de tu contenedor.
- Utiliza ambos juntos para obtener una documentación y funcionalidad completas, especialmente en entornos de producción.
Cómo exponer puertos Docker
Ahora que entendemos los conceptos, veamos los aspectos prácticos de la exposición de puertos Docker en diferentes contextos.
Estrategias de declaración de Dockerfile
La forma más básica de exponer un puerto es utilizando la instrucción EXPOSE
en tu Dockerfile. Declara los puertos que debe utilizar el contenedor.
FROM my-image:latest
EXPOSE 80
EXPOSE 443
En este ejemplo, he especificado que el contenedor utilizará los puertos 80
y 443
, que son estándar para el tráfico HTTP (Protocolo de Transferencia de Hipertexto) y HTTPS (Protocolo de Transferencia de Hipertexto Seguro). También puedes combinarlas en una sola instrucción:
EXPOSE 80 443
Al especificar los puertos, es una buena práctica incluir el protocolo, TCP (Protocolo de Control de Transmisión) o UDP (Protocolo de Datagramas de Usuario) si tu aplicación utiliza uno específico:
EXPOSE 53/udp
EXPOSE 80/tcp
Si no se especifica ningún protocolo, se asume TCP por defecto. Esto es apropiado para la mayoría de las aplicaciones web, pero servicios como DNS (Sistema de Nombres de Dominio) o servidores de juegos suelen requerir UDP.
Desde el punto de vista de la seguridad, te recomiendo que sólo expongas los puertos que tu aplicación necesita realmente. Cada puerto expuesto representa un vector de ataque potencial, por lo que minimizar el número de puertos expuestos sigue el principio del mínimo privilegio.
Gestión de puertos en tiempo de ejecución
Aunque la instrucción EXPOSE
documenta qué puertos utiliza un contenedor, para que estos puertos sean accesibles desde el host u otras máquinas, tienes que publicarlos al ejecutar el contenedor.
He aquí cómo vincular explícitamente un puerto de contenedor a un puerto de host específico:
docker run -p 8081:80 my-image
La bandera -p
(o --publish
) te permite vincular un único puerto o rango de puertos del contenedor al host.
Este comando asigna el puerto 80 dentro del contenedor al puerto 8081 en el host. Después de ejecutar este comando, puedes acceder al servidor web navegando a http://localhost:8081
en tu navegador.
Para mayor comodidad durante el desarrollo, Docker proporciona la bandera -P
, P mayúscula o --publish-all
, que publica automáticamente todos los puertos expuestos en puertos aleatorios con números altos del host:
docker run -P my-image
Para saber qué puertos host se asignaron, puedo utilizar el comando docker ps
:
docker ps
Esto mostrará resultados como:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a7c53d9413bf image "/docker-.…" 10 s ago Up 9 s 0.0.0.0:49153->80/tcp, :::49153->80/tcp wizardl
Aquí, el puerto 80 dentro del contenedor se asigna al puerto 49153 en el host.
Cuando trabajes con servicios que utilicen tanto TCP como UDP en el mismo puerto (como los servidores DNS), deberás especificar el protocolo al publicar:
docker run -p 53:53/tcp -p 53:53/udp dns-server
Orquestación Docker Compose
Para las aplicaciones multicontenedor, Docker Compose proporciona una forma más manejable de configurar la red y los puertos.
En un archivo YAML de Docker Compose, puedes especificar asignaciones de puertos bajo la clave ports
para cada servicio:
services:
web:
image: my-image
ports:
- "8081:80"
- "443:443"
api:
build: ./api
ports:
- "3000:3000"
Docker Compose hace una distinción importante entre ports
y expose
. La sección ports
crea mapeos de puertos publicados accesibles desde el exterior, mientras que expose
sólo pone puertos a disposición de los servicios enlazados dentro de la misma red Compose:
services:
web:
image: my-image
ports:
- "8081:80"
database:
image: postgres
expose:
- "5432"
En este ejemplo, el servicio web
es accesible desde el host en el puerto 8081, pero la base de datos PostgreSQL sólo es accesible para otros servicios dentro del archivo Compose, no directamente desde el host.
Para configuraciones flexibles, especialmente en distintos entornos, puedo utilizar variables de entorno en las asignaciones de puertos:
services:
web:
image: my-image
ports:
- "${WEB_PORT:-8081}:80"
Esta sintaxis me permite especificar el puerto del host a través de una variable de entorno (WEB_PORT
), por defecto 8081 si no se establece.
Exponer puertos Docker - Ejemplo práctico
Veamos un ejemplo completo de exposición de puertos Docker para una aplicación web con un backend de base de datos.
Imagina que estamos construyendo una aplicación web sencilla que consiste en un servicio API Flask de Python y una base de datos PostgreSQL. El servicio API escucha en el puerto 5000
, y PostgreSQL utiliza su puerto por defecto 5432
.
Este es el aspecto que podría tener nuestra sencilla aplicación Flask (app.py
). Esta aplicación creará una base de datos myapp
y obtendrá los datos de la tabla items
, una vez creada esta tabla dentro de la base de datos.
from flask import Flask, jsonify
import os
import psycopg2
app = Flask(__name__)
# Database connection parameters from environment variables
DB_HOST = os.environ.get('DB_HOST', 'db')
DB_NAME = os.environ.get('DB_NAME', 'myapp')
DB_USER = os.environ.get('DB_USER', 'postgres')
DB_PASS = os.environ.get('DB_PASSWORD', 'postgres')
@app.route('/')
def index():
return jsonify({'message': 'API is running'})
@app.route('/items')
def get_items():
# Connect to the PostgreSQL database
conn = psycopg2.connect(
host=DB_HOST,
database=DB_NAME,
user=DB_USER,
password=DB_PASS
)
# Create a cursor and execute a query
cur = conn.cursor()
cur.execute('SELECT id, name FROM items')
# Fetch results and format as list of dictionaries
items = [{'id': row[0], 'name': row[1]} for row in cur.fetchall()]
# Close connections
cur.close()
conn.close()
return jsonify(items)
if __name__ == '__main__':
# Run the Flask app, binding to all interfaces (important for Docker)
app.run(host='0.0.0.0', port=5000)
A continuación, crearé nuestro archivo requirements.txt
:
flask
psycopg2-binary
Ahora tengo que crear un archivo Dockerfile para nuestro servicio API de Python:
FROM python:3.9-slim
WORKDIR /app
# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose the port Flask runs on
EXPOSE 5000
# Command to run the application
CMD ["python", "app.py"]
Por último, crearé un archivo Docker Compose para orquestar ambos servicios:
services:
api:
build:
context: .
dockerfile: Dockerfile
tags:
- "my-custom-image:latest"
container_name: my-custom-api
ports:
- "5000:5000"
environment:
- DB_HOST=db
- DB_NAME=myapp
- DB_USER=postgres
- DB_PASSWORD=postgres
depends_on:
- db
db:
container_name: my-custom-db
image: postgres:13
expose:
- "5432"
environment:
- POSTGRES_DB=myapp
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
En esta configuración:
- El servicio API se construye a partir de nuestro Dockerfile y publica el puerto
5000
, haciéndolo accesible desde la máquina anfitriona. - El servicio PostgreSQL expone el puerto
5432
pero no lo publica, por lo que sólo es accesible para otros servicios de la red Compose. - El servicio API puede acceder a la base de datos utilizando el nombre de host db (que es el nombre del servicio) y el puerto
5432
.
Para ejecutar esta aplicación, utiliza
docker-compose up
Ahora puedo acceder a la API en http://localhost:5000
, pero no se puede acceder directamente a la instancia PostgreSQL desde fuera de la red Docker, lo cual es una buena práctica de seguridad para los servicios de bases de datos. Pero puedes acceder a PostgreSQL desde el contenedor en ejecución:
docker compose exec db psql -U postgres -d myapp
Este comando accederá a la CLI de PostgreSQL, donde puedo ver la base de datos y crear una tabla items
. Una vez creado, puedo ver los datos en http://localhost:5000/items
Cómo exponer varios puertos Docker
Si mi aplicación Python necesita exponer varios puertos, puedo especificarlos todos en el archivo Dockerfile:
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000 8081 9090
CMD ["python", "app.py"]
Este Dockerfile expone:
- Puerto
5000
para la aplicación principal de Flask (actualmente activa) - Puerto
8081
para un posible cuadro de mandos de vigilancia - Puerto
9090
para una futura interfaz de depuración
Es importante señalar que, aunque he expuesto los puertos 8081
y 9090
en el Dockerfile como documentación, mi aplicación Flask actual sólo está configurada para escuchar en el puerto 5000
. Los puertos adicionales expuestos indican dónde podrían implementarse futuras funcionalidades.
Cómo publicar puertos expuestos
Para publicar los puertos expuestos en un Dockerfile, utilizo la bandera -p
o -P
al ejecutar el contenedor:
docker run -p 8081:5000 my-custom-image
Esto asigna el puerto 5000
en el contenedor (que está expuesto en el Dockerfile y donde Flask está escuchando) al puerto 8081
en el host. Después de ejecutar este comando, puedo acceder a mi aplicación Flask navegando a http://localhost:8081
en mi navegador.
También puedo utilizar la bandera -P para publicar automáticamente todos los puertos expuestos en puertos aleatorios del anfitrión:
docker run -P my-custom-image
Para comprobar qué puertos están publicados, puedo utilizar:
docker ps
Para obtener información más detallada, puedo utilizar
docker port [container_id]
5000/tcp -> 0.0.0.0:32768
8081/tcp -> 0.0.0.0:32769
9090/tcp -> 0.0.0.0:32770
Este comando muestra todas las asignaciones de puertos de un contenedor concreto. Por ejemplo, si mi aplicación Flask sólo escucha en el puerto 5000
pero he expuesto varios puertos en el Dockerfile, Docker port me mostrará exactamente qué puertos del host están asignados a qué puertos del contenedor. Así, si el puerto 5000
se ha asignado a 32768, puedo acceder a mi aplicación Flask bajo http://localhost:32768
Técnicas de diagnóstico y resolución de problemas
Incluso con una configuración cuidadosa, pueden surgir problemas con la asignación de puertos Docker. He aquí algunas técnicas que utilizo para diagnosticar y resolver problemas comunes.
Inspección de la cartografía portuaria
El primer paso para diagnosticar problemas relacionados con los puertos es verificar que las asignaciones están configuradas correctamente. Utilizo estos comandos:
Lista todos los contenedores en ejecución con asignaciones de puertos:
docker ps
Obtén información detallada sobre un contenedor concreto:
docker inspect [container_id]
Comprueba las asignaciones de puertos de un contenedor concreto:
docker port [container_id]
El comando docker inspect proporciona información detallada, incluida la configuración de red. Para centrarte en los detalles relacionados con la red, utiliza
docker inspect --format='{{json .NetworkSettings.Ports}}' [container_id] | jq
Si necesitas realizar un análisis más profundo del tráfico, herramientas como tcpdump
y netstat
pueden serte muy útiles. En primer lugar, asegúrate de que están instalados dentro del contenedor. Si el contenedor aún no incluye estas herramientas, puedes instalarlas ejecutando el siguiente comando:
docker exec -u root -it f5c6cac71492 bash -c "apt-get update && apt-get install -y net-tools tcpdump"
Para ver en qué puertos está escuchando la aplicación dentro del contenedor:
docker exec -it [container_id] netstat -tuln
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:5000 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.11:42265 0.0.0.0:* LISTEN
udp 0 0 127.0.0.11:34267 0.0.0.0:*
Esto muestra, por ejemplo, que la aplicación Flask está escuchando en todas las interfaces en el puerto 5000
.
Para controlar el tráfico entrante y saliente en la interfaz de red principal del contenedor (normalmente eth0):
docker exec -it [container_id] tcpdump -i eth0
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
Este comando te permite rastrear el tráfico e identificar si las peticiones están llegando al contenedor o si los paquetes se están descartando o desviando.
Análisis de reglas iptables
Docker aprovecha iptables
para gestionar la Traducción de Direcciones de Red (NAT) y controlar el flujo de tráfico entre el host y los contenedores. Inserta automáticamente reglas para reenviar los puertos del host a las IP y puertos apropiados del contenedor, permitiendo un acceso sin fisuras a los servicios en contenedores.
En hosts Linux nativos, puedes inspeccionar estas reglas utilizando comandos como:
sudo iptables -t nat -L DOCKER -n -v
Esto muestra cómo Docker asigna el tráfico entrante en puertos específicos del host a los puntos finales del contenedor. Basándote en el archivo Docker Compose de antes, verás los puertos reenviados:
Chain DOCKER (2 references)
pkts bytes target prot opt in out source destination
50 3000 RETURN all -- docker0 * 0.0.0.0/0 0.0.0.0/0
100 6000 RETURN all -- br-xxxxxxx * 0.0.0.0/0 0.0.0.0/0
500 30000 DNAT tcp -- !br-xxxxxxx * 0.0.0.0/0 0.0.0.0/0 tcp dpt:5000 to:172.18.0.2:5000
400 24000 DNAT tcp -- !br-xxxxxxx * 0.0.0.0/0 0.0.0.0/0 tcp dpt:8081 to:172.18.0.2:5000
300 18000 DNAT tcp -- !br-xxxxxxx * 0.0.0.0/0 0.0.0.0/0 tcp dpt:9090 to:172.18.0.2:5000
Toma:
DNAT
redirigen el tráfico del host al contenedor.RETURN
garantizan el correcto enrutamiento del tráfico interno de la red Docker.
Sin embargo, dependiendo de tu entorno (por ejemplo, Docker Desktop en Windows o WSL2), Docker puede gestionar el reenvío de puertos de forma diferente, y las reglas de iptables
pueden no ser visibles o modificables desde dentro del sistema. En este caso, puedes comprobar el reenvío de puertos con el siguiente comando:
sudo ss -tulnp
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 0 4096 *:5000 *:*
tcp LISTEN 0 4096 *:8081 *:*
tcp LISTEN 0 4096 *:9090 *:*
Consejos para solucionar problemas:
- Confirma las asignaciones de puertos con
docker ps
ydocker port [container]
. - Comprueba si el cortafuegos del sistema anfitrión o el software de seguridad está bloqueando puertos.
- Utiliza los registros del contenedor y herramientas de red como
netstat
ytcpdump
para verificar el comportamiento de la red de tu aplicación.
Conclusión
Exponer puertos Docker es una habilidad fundamental que tiende un puente entre el aislamiento en contenedores y la usabilidad práctica. A lo largo de este artículo, he explicado cómo funciona el modelo de red de Docker, la diferencia entre exponer y publicar puertos, y he proporcionado ejemplos prácticos para diversos escenarios.
Ten en cuenta estos puntos clave:
- Utiliza
EXPOSE
en Dockerfiles para documentar qué puertos utiliza tu aplicación - Utiliza
-p
o-P
cuando ejecutes contenedores para que esos puertos sean accesibles desde el exterior - Aprovecha Docker Compose para gestionar aplicaciones complejas multicontenedor
- Utiliza herramientas de diagnóstico para solucionar problemas cuando surjan
Dominar estos conceptos y técnicas te permitirá diseñar aplicaciones en contenedores más robustas que se comuniquen eficazmente tanto internamente como con el mundo exterior. Tanto si estás desarrollando una sencilla aplicación web como una compleja arquitectura de microservicios, una correcta gestión de puertos es fundamental para el éxito.
A medida que los contenedores sigan dominando las estrategias modernas de despliegue de aplicaciones, la capacidad de gestionar con confianza las redes y puertos Docker seguirá siendo una habilidad esencial en el conjunto de herramientas de todo programador.
Para seguir aprendiendo, no dejes de consultar los siguientes recursos:
Preguntas frecuentes sobre la exposición de un puerto Docker
¿Qué significa exponer un puerto en Docker?
Exponer un puerto en Docker es una forma de documentar en qué puertos escucha tu aplicación en contenedor, pero no hace que el puerto sea accesible fuera del contenedor.
¿Cómo hago que el puerto de un contenedor Docker sea accesible desde mi máquina anfitriona?
Puedes publicar un puerto utilizando la bandera -p
con docker run
o la sección ports
en Docker Compose, que asigna un puerto de host al puerto del contenedor.
¿Cuál es la diferencia entre `EXPOSE` y `-p` en Docker?
EXPOSE
se utiliza en el Dockerfile para declarar puertos para la documentación, mientras que -p
publica y mapea esos puertos para el acceso externo.
¿Puedo exponer varios puertos para un único contenedor Docker?
Sí, puedes exponer varios puertos utilizando varias instrucciones EXPOSE
en el Dockerfile o especificando varios puertos en la sección ports
de Docker Compose.
¿Cómo puedo solucionar los problemas de mapeo de puertos de Docker?
Utiliza comandos como docker ps
, docker port
, docker inspect
, y herramientas de red dentro del contenedor (por ejemplo, netstat
, tcpdump
) para verificar las asignaciones de puertos y el flujo de tráfico.
Como fundador de Martin Data Solutions y científico de datos autónomo, ingeniero de ML e IA, aporto una cartera diversa en Regresión, Clasificación, PNL, LLM, RAG, Redes Neuronales, Métodos de Ensemble y Visión por Ordenador.
- Desarrolló con éxito varios proyectos de ML de extremo a extremo, incluyendo la limpieza de datos, análisis, modelado y despliegue en AWS y GCP, ofreciendo soluciones impactantes y escalables.
- Construí aplicaciones web interactivas y escalables utilizando Streamlit y Gradio para diversos casos de uso de la industria.
- Enseñó y tuteló a estudiantes en ciencia de datos y analítica, fomentando su crecimiento profesional mediante enfoques de aprendizaje personalizados.
- Diseñó el contenido del curso para aplicaciones de generación aumentada por recuperación (RAG) adaptadas a los requisitos de la empresa.
- Es autora de blogs técnicos de IA y ML de gran impacto, que tratan temas como MLOps, bases de datos vectoriales y LLMs, logrando un compromiso significativo.
En cada proyecto que asumo, me aseguro de aplicar prácticas actualizadas en ingeniería de software y DevOps, como CI/CD, code linting, formateo, monitorización de modelos, seguimiento de experimentos y una sólida gestión de errores. Me comprometo a ofrecer soluciones completas, convirtiendo los datos en estrategias prácticas que ayuden a las empresas a crecer y a sacar el máximo partido de la ciencia de datos, el aprendizaje automático y la IA.