Saltar al contenido
sudoquest@root
Docker

Puertos y redes

Publicar el puerto de un contenedor, saber quién escucha dónde, redes propias para que se llamen por su nombre, y por qué ufw no te protege.

25 min de lectura

Un contenedor tiene su propia red: su IP, sus puertos, su localhost. Por eso un nginx en marcha dentro de un contenedor no se ve desde fuera hasta que publicas su puerto, y por eso dos contenedores no se encuentran por su nombre si no están en la misma red propia. En este tema aprendes las dos cosas, y a averiguar quién escucha en cada puerto.

Los ejercicios pasan en el servidor de pruebas de Cines Lumière, una cadena de cines: la web de la cartelera, la API de las sesiones y la base de datos de las reservas, cada una en su contenedor.

Publicar un puerto: -p

Un nginx arrancado sin más escucha en el puerto 80… de su contenedor. docker ps lo dice en PORTS (80/tcp, sin flecha: no está publicado) y desde la máquina no llega nada:

tecnico@srv-pruebas:~$ curl -s -m 2 localhost:8080; echo "codigo $?"
codigo 7

(El 7 de curl es “no he podido conectar”.) Con -p máquina:contenedor, Docker abre ese puerto en la máquina y lo lleva al del contenedor. Primero va el de la máquina:

tecnico@srv-pruebas:~$ docker run -d --name taquilla -p 8080:80 nginx:1.27-alpine
07b361956bfb74a2ea66f68dfea81bf8ec8aebdace4265d4d718a5b375b45b57
tecnico@srv-pruebas:~$ docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED        STATUS                  PORTS                                     NAMES
07b361956bfb   nginx:1.27-alpine   "/docker-entrypoint.…"   1 second ago   Up Less than a second   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   taquilla

0.0.0.0:8080->80/tcp se lee “el 8080 de todas las direcciones de la máquina va al 80 del contenedor” (y [::], lo mismo en IPv6). Ahora sí:

tecnico@srv-pruebas:~$ curl -s localhost:8080 | head -n 4
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Con -p 80 (solo el del contenedor), Docker elige un puerto libre de la máquina (del 32768 en adelante); con -P, publica así todos los puertos de la imagen.

Ejercicio

Que se vea desde fuera

La web de la cartelera tiene que verse desde fuera del servidor, en su puerto 8080. Arranca con nginx:1.27-alpine el contenedor cartelera publicando su puerto 80 en el 8080 de la máquina, y compruébalo desde la máquina.

Preparando la máquina…

¿Dónde escucha cada uno?

docker port enseña lo publicado de un contenedor:

tecnico@srv-pruebas:~$ docker port taquilla
80/tcp -> 0.0.0.0:8080
80/tcp -> [::]:8080

Y desde la máquina, ss enseña quién tiene el puerto: un docker-proxy por cada publicado (es el programa de Docker que recoge lo que llega a la máquina y lo pasa al contenedor):

tecnico@srv-pruebas:~$ sudo ss -tlnp | grep -E "8080|State"
State  Recv-Q Send-Q Local Address:Port Peer Address:PortProcess
LISTEN 0      4096         0.0.0.0:8080      0.0.0.0:*    users:(("docker-proxy",pid=2804,fd=7))
LISTEN 0      4096            [::]:8080         [::]:*    users:(("docker-proxy",pid=2809,fd=7))

Un puerto de la máquina solo puede tenerlo uno. Si otro contenedor ya lo tiene, Docker lo dice al arrancar (el contenedor se queda creado, sin arrancar):

tecnico@srv-pruebas:~$ docker run -d --name taquilla2 -p 8080:80 nginx:1.27-alpine
7ecf74f66d971dda6326922b5e7989d85f25dca0e36c14843f7a80ae6a7ba0da
docker: Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint taquilla2 (a5ff90c890c4ad4d89b3f1a90500678217ae76c1afbcb2036c529a786588e624): Bind for 0.0.0.0:8080 failed: port is already allocated

Run 'docker run --help' for more information

Si el que lo tiene es un programa de la máquina (no un contenedor), el final dice otra cosa: address already in use. En los dos casos, docker ps (o sudo ss -tlnp) te dice quién es. El contenedor que no arrancó sigue ahí (Created): bórralo con docker rm antes de volver a crearlo con el mismo nombre. O, si ya tiene el -p que tiene que tener, libera el puerto y arráncalo con docker start: un contenedor guarda lo que se publica, y lo vuelve a pedir cada vez que arranca (también uno parado al que otro le ha quitado el puerto mientras tanto; el error dice cuál es).

Ejercicio

El puerto está cogido

Al arrancar la cartelera en el 8080, Docker dice que el puerto ya está ocupado. Averigua qué contenedor lo tiene y arranca la cartelera en el 8081, sin tocar ese otro.

Preparando la máquina…

Solo para esta máquina

-p 5432:5432 publica en todas las direcciones de la máquina: también en la que da a internet. Una base de datos que solo usan programas del propio servidor se publica solo en 127.0.0.1, poniendo la IP delante:

tecnico@srv-pruebas:~$ docker run -d --name db -e POSTGRES_PASSWORD=secreto -p 127.0.0.1:5432:5432 postgres
6f60e535f4f6967c1262a6cf0b2a71381b891e609de33bbe89edbe50b974e206
tecnico@srv-pruebas:~$ docker port db
5432/tcp -> 127.0.0.1:5432

Ejercicio

Solo para esta máquina

La base de datos de las reservas no puede verse desde fuera: solo la usan programas del propio servidor. Arranca postgres en el contenedor reservas-db, con la contraseña que quieras (POSTGRES_PASSWORD), publicando su 5432 solo en 127.0.0.1.

Preparando la máquina…

La red de serie: sin nombres

Cada contenedor arrancado sin más está en la red de serie, bridge (en la máquina, la interfaz docker0, 172.17.0.1). Ahí los contenedores se ven por su IP, pero no por su nombre:

tecnico@srv-pruebas:~$ docker run --rm alpine ping -c 1 -W 1 web
ping: bad address 'web'

Por su IP sí llega, pero la IP cambia cada vez que un contenedor arranca: no sirve para que una API encuentre su base de datos.

Redes propias: docker network

Una red creada por ti tiene su DNS: dentro de ella, cada contenedor se encuentra por su nombre.

tecnico@srv-pruebas:~$ docker network create cines
c504e593c610d013797d2ab0eb24bd15aeb7c2e4d3919f4f7a7e4d67bba4d158
tecnico@srv-pruebas:~$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
39cc508809d4   bridge    bridge    local
c504e593c610   cines     bridge    local
8760ef4471c7   host      host      local
c942a5c9eff2   none      null      local

Con --network el contenedor nace en ella; desde otro de la misma red, se le llama por su nombre:

tecnico@srv-pruebas:~$ docker run -d --name api --network cines nginx:1.27-alpine
tecnico@srv-pruebas:~$ docker run --rm --network cines alpine ping -c 1 -W 1 api
PING api (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.089 ms

--- api ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max = 0.089/0.089/0.089 ms

Quien contesta el nombre es el DNS de Docker: dentro de una red propia, el /etc/resolv.conf del contenedor dice nameserver 127.0.0.11.

Ejercicio

Por su nombre

En la red de serie, los contenedores no se encuentran por su nombre. Crea la red cines, arranca en ella el contenedor api (con nginx:1.27-alpine) y, desde un alpine en la misma red, pide http://api.

Preparando la máquina…

Conectar y desconectar

Un contenedor puede estar en varias redes. docker network connect mete en otra a uno que ya está en marcha (sin pararlo), y docker network inspect dice quién hay en cada una (en Containers):

tecnico@srv-pruebas:~$ docker network connect cines web
tecnico@srv-pruebas:~$ docker ps --filter network=cines
CONTAINER ID   IMAGE               COMMAND                  CREATED          STATUS          PORTS     NAMES
e942f04a00df   nginx:1.27-alpine   "/docker-entrypoint.…"   7 seconds ago    Up 7 seconds    80/tcp    api
879a961dea5e   nginx:1.27-alpine   "/docker-entrypoint.…"   14 seconds ago   Up 14 seconds   80/tcp    web

docker network disconnect cines web lo saca.

Ejercicio

Uno que ya estaba en marcha

La web lleva días en marcha en la red de serie y ahora tiene que hablar con la api, que está en la red cines. Conéctala a esa red sin pararla, y comprueba que la red ya las tiene a las dos.

Preparando la máquina…

Borrar redes

Una red con contenedores en marcha no se puede borrar:

tecnico@srv-pruebas:~$ docker network rm cines
Error response from daemon: error while removing network: network cines id c504e593c610d013797d2ab0eb24bd15aeb7c2e4d3919f4f7a7e4d67bba4d158 has active endpoints
exit status 1

Los “endpoints activos” son los contenedores que siguen en ella. Desconéctalos, páralos o bórralos, y entonces docker network rm cines funciona. docker network prune borra de una vez las propias que no usa nadie (las de serie, bridge, host y none, no se borran nunca).

Ejercicio

La red que sobra

La red pruebas ya no hace falta, pero docker network rm dice que tiene endpoints activos. Bórrala; el contenedor prueba-taquilla que sigue en ella también sobra.

Preparando la máquina…

host y none

Además de bridge, hay dos redes de serie más:

  • --network host: el contenedor usa la red de la máquina tal cual. Su nginx escucha en el 80 de la máquina, sin -p (Docker ignora lo publicado) y sin red propia.
  • --network none: sin red. Solo lo.

Resumen

Orden Para qué
docker run -p 8080:80 … publicar el 80 del contenedor en el 8080 de la máquina
docker run -p 127.0.0.1:5432:5432 … publicarlo solo para la propia máquina
docker port c · sudo ss -tlnp dónde escucha cada uno
docker network create red una red propia (con nombres)
docker run --network red … arrancar en ella
docker network connect red c / disconnect meter o sacar uno que ya existe
docker network inspect red quién hay en ella
docker network rm red · prune borrarla (sin contenedores en marcha)