Clase 2 / 3
DOCKER Clase 2

Docker para el día a día

Gestionar y compartir imágenes, persistir datos con volúmenes, conectar contenedores en red y levantarlo todo con Docker Compose

🎬
VER VIDEO YOUTUBE - CLASE 2 Imágenes, volúmenes, redes y Docker Compose

1. Gestionar imágenes

Crear y exportar

En la Clase 1 ya creamos imágenes con docker build y las vimos con docker images. Ahora toca lo que hacemos con ellas: etiquetarlas, compartirlas y moverlas de una máquina a otra.

Si ya tenemos un contenedor en ejecución, podemos crear una imagen a partir de su estado actual con docker commit:

docker commit ID-CONTENEDOR NOMBRE-IMAGEN

Aviso: esto es un atajo, no la forma buena de crear imágenes. Lo correcto es el Dockerfile de la Clase 1, porque es reproducible; el commit es una foto manual que nadie sabe cómo se hizo. Que sepas que existe, pero no cojas el hábito.

Todas las imágenes —creadas, descargadas o commiteadas— se pueden exportar a un fichero .tar para llevarlas a otro sistema:

docker save -o fichero.tar imagen

Y para importarlas en la máquina de destino:

docker load -i fichero.tar
docker save y docker load

Etiquetas: el tag de una imagen

Una imagen se identifica con nombre:version. Esa versión es la etiqueta (tag), y la podemos indicar al construir con el argumento -t del docker build.

También podemos añadir un nombre y etiqueta nuevos a una imagen ya existente:

docker tag <nombre_imagen>:<etiqueta> <nuevo_nombre>:<nueva_etiqueta>

Nota: docker tag no duplica la imagen, solo le añade otro nombre que apunta a la misma. Si no indicamos etiqueta, Docker asume :latest.

Compartir imágenes: push y pull

Para compartir una imagen la subimos a DockerHub, el repositorio donde hay miles de imágenes públicas. Se sube con:

docker push USUARIO/imagen:tag

Importante: el usuario debe ser el mismo con el que nos hemos registrado, y hay que haber iniciado sesión antes con docker login. Es decir, la imagen debe llamarse ya usuario/imagen:tag (si no, la renombramos con docker tag).

A partir de ahí, cualquiera la descarga igual que hicimos con las imágenes públicas en la Clase 1:

docker pull usuario/miweb:1.0

También se pueden subir a otros repositorios remotos; en ese caso el nombre debe incluir la dirección del registro, por ejemplo con GitHub.

2. Volúmenes

Hay que entender una cosa clave: un contenedor no guarda su estado. Todo lo que se escribe dentro vive solo mientras el contenedor existe; en cuanto lo borras, esos datos desaparecen. Para una base de datos o cualquier fichero que deba sobrevivir, eso no vale.

La solución son los volúmenes: almacenamiento que vive fuera del contenedor y que montamos dentro. Así podemos borrar y recrear el contenedor, pero los datos permanecen. Físicamente Docker los guarda en su propia carpeta del sistema (/var/lib/docker/volumes), pero la gracia es justo esa: no te tienes que preocupar de dónde están, los gestiona él.

docker volume create mi-vol
docker volume ls
docker volume inspect mi-vol
Crear e inspeccionar un volumen

Para montarlos usamos el argumento -v al lanzar docker run, y hay dos formas:

TipoSintaxisPara qué
Volumen-v nombre:/rutaAlmacenamiento gestionado por Docker. El habitual para datos.
Bind mount-v /ruta/host:/ruta/contenedorMonta una carpeta real de tu máquina dentro del contenedor.

El volumen lo gestiona Docker y es el que usarás para persistir datos de verdad (la base de datos, por ejemplo). El bind mount apunta a una carpeta concreta de tu host y es cómodo para desarrollo: editas un fichero en tu máquina y el contenedor lo ve al instante.

El truco para distinguirlos es la barra: si empieza por / es una ruta de tu máquina, o sea un bind mount; si es solo un nombre, es un volumen de Docker.

Un ejemplo con volumen:

docker run -d --name db -v datos:/var/lib/mysql mariadb

Un ejemplo con bind mount:

docker run -d --name web -v /home/usuario/web:/usr/share/nginx/html nginx
Contenedor con un volumen montado

Aunque borremos el contenedor db, el volumen datos sigue ahí con la información, listo para montarlo en otro. La prueba, en tres pasos:

docker rm -f db                                            # borramos el contenedor
docker run -d --name db -v datos:/var/lib/mysql mariadb    # lo recreamos igual
docker exec -it db mysql -uroot -p                         # los datos siguen ahí

Para borrar un volumen que ya no usamos:

docker volume rm mi-vol

Copiar ficheros puntuales con docker cp

Si lo único que quieres es sacar o meter un fichero concreto en un contenedor, no hace falta montar un volumen. Para eso está docker cp, que copia igual que el cp de siempre pero entre tu máquina y el contenedor:

docker cp db:/etc/mysql/my.cnf .      # del contenedor a tu carpeta
docker cp ./config.txt db:/tmp/       # de tu carpeta al contenedor
docker cp entre host y contenedor

3. Redes

Por defecto, cada contenedor está aislado. Cuando tenemos varios que deben hablar entre sí (una web y su base de datos, por ejemplo), los ponemos en la misma red.

docker network create red
docker network ls
docker network inspect red
Crear y listar redes

El driver que vas a usar casi siempre es bridge — es más, es el que se pone por defecto si no dices nada. Para empezar usaremos casi siempre bridge, pero podríamos ver otras opciones. Para poner una en específico podríamos indicar docker network create --driver bridge red. Las que existen son:

DriverQué hace
bridgeRed predeterminada que conecta contenedores dentro de una misma máquina.
hostEl contenedor usa directamente la red del anfitrión. Más rápido, pero menos aislamiento.
overlayConecta contenedores que se ejecutan en máquinas diferentes.
macvlanEl contenedor aparece en la red como un dispositivo independiente, con su propia MAC.
noneDesactiva la red del contenedor, dejándolo sin conexión externa.

Esta red la podemos usar al lanzar el contenedor:

docker run -d --name web --network red nginx

También podemos conectar una red a un contenedor ya existente (tanto la red como el contenedor deben existir previamente):

docker network connect mi-red mi-contenedor
docker network disconnect mi-red mi-contenedor

Cuando un contenedor necesita hablar con otro, necesita saber dónde está: normalmente su IP. El problema es que las IPs de los contenedores cambian — paras uno y al arrancarlo Docker puede darle otra.

Dentro de una red creada por nosotros esto se soluciona solo: cada contenedor es alcanzable por su propio nombre, y Docker se encarga de traducirlo a la IP que tenga en ese momento. Como el nombre nunca cambia, siempre funciona. Eso sí, sin una red propia los contenedores no es que no se vean — se ven, pero solo por IP.

Por ejemplo:

docker run -dit --name linux --network red alpine
docker exec -it linux ping web
Resolución por nombre entre contenedores

Por último, para borrar una red:

docker network rm red

4. Docker Compose

Hasta ahora, para levantar una web con su base de datos hemos tenido que lanzar varios docker run, crear la red, crear el volumen… a mano y en orden. Docker Compose nos permite describir todo eso en un único fichero y levantarlo con un solo comando.

El fichero se llama compose.yaml y describe los servicios (contenedores), sus volúmenes y su configuración. Un ejemplo sencillo:

Ejemplo de fichero compose.yaml

Tenemos el servicio web que está usando la imagen nginx, publicando el puerto 80 del contenedor en el 8080 del host.

Podemos comprobar que no describimos pasos, describimos el resultado que queremos: qué servicios existen y cómo son. Docker se encarga del cómo.

Levantamos todos los servicios de una vez, y una vez levantado con Compose, dejamos de usar docker ps o docker logs y tiramos de los del propio Compose, que actúan sobre todos los servicios del fichero a la vez:

docker compose up -d
docker compose -f /ruta/al/compose.yaml up -d    # si el fichero está en otra ruta

docker compose ps
docker compose logs -f
docker compose exec web sh
docker compose up, ps y logs

En estos comandos usas el nombre del servicio (web), no el del contenedor que ves en docker ps — Compose razona por servicios.

Construir un Dockerfile desde Compose

En vez de usar una imagen ya hecha, podemos pedirle a Compose que construya la nuestra. Si tenemos un Dockerfile en la misma carpeta que el fichero compose, solo hay que añadir la clave build en el servicio e indicar su ruta:

services:
  web:
    build: .
    ports:
      - "8080:80"

Ese . significa "el Dockerfile de esta misma carpeta". Con docker compose up -d --build forzamos que lo reconstruya cuando cambiemos algo.

Un ejemplo más completo

Ejemplo de compose con varios servicios Servicios levantados con Compose

Volúmenes y redes en Compose

Los volúmenes y las redes se declaran en su propia sección y luego se referencian en cada servicio que los use. Un ejemplo completo con nginx como web y redis como su almacén, con red y un volumen:

Compose con volúmenes y redes declarados Compose levantando los servicios con su red y volumen

Abajo se declaran el volumen y la red. Al volumen lo referencia el servicio de redis, indicando la ruta donde se monta dentro del contenedor (/data, donde redis guarda su información). La red la comparten los dos servicios, y por eso el servicio web puede encontrar a redis por su nombre.

Al declararlos solo con el nombre, Docker los crea con sus valores por defecto: un volumen normal gestionado por él y una red de tipo bridge. Se pueden personalizar, pero para la mayoría de casos con esto sobra.

De hecho, si no declaramos ninguna red, Compose crea una automáticamente y mete todos los servicios dentro — por eso el primer ejemplo ya funcionaba sin escribir nada de redes. Esa red toma el nombre de la carpeta del proyecto seguido de _default (por ejemplo miproyecto_default), y puedes verla con docker network ls o mirarla a fondo con docker network inspect, como vimos antes.

Lo más importante a tener en cuenta es que en Compose describimos qué queremos que exista —estos servicios, este volumen, esta red— y Docker lo monta. No le decimos cómo, le decimos qué. Esto funciona de la misma manera en Kubernetes.

Como vemos al final de la última imagen, podemos parar y eliminar todo lo que levantamos (contenedores y red):

docker compose down

Si además queremos borrar los volúmenes, usamos docker compose down -v — pero cuidado, que eso se lleva los datos por delante.