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
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
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
Para montarlos usamos el argumento -v al lanzar docker run, y hay dos formas:
| Tipo | Sintaxis | Para qué |
|---|---|---|
| Volumen | -v nombre:/ruta | Almacenamiento gestionado por Docker. El habitual para datos. |
| Bind mount | -v /ruta/host:/ruta/contenedor | Monta 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
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
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
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:
| Driver | Qué hace |
|---|---|
bridge | Red predeterminada que conecta contenedores dentro de una misma máquina. |
host | El contenedor usa directamente la red del anfitrión. Más rápido, pero menos aislamiento. |
overlay | Conecta contenedores que se ejecutan en máquinas diferentes. |
macvlan | El contenedor aparece en la red como un dispositivo independiente, con su propia MAC. |
none | Desactiva 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
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:
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
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
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:
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.