Fundamentos de Docker
Qué es Docker, sus piezas básicas, instalación y tus primeros contenedores e imágenes
1. Qué es Docker
Docker es una herramienta que empaqueta tu aplicación junto con todo lo que necesita para funcionar —su código, sus librerías y su configuración— en una unidad única y aislada llamada contenedor, que se ejecuta igual en cualquier máquina.
Esa es la idea clave: si funciona en tu portátil, funciona igual en el servidor, porque el entorno viaja dentro del propio contenedor.
2. Diferencias entre una máquina virtual y Docker
Una máquina virtual ejecuta un sistema operativo completo virtualizado, gestionado por un hipervisor como VMware o KVM. Un contenedor, en cambio, no lleva un sistema completo: comparte el kernel de la máquina donde corre y solo empaqueta lo que la aplicación necesita.
- Máquina virtual → kernel propio, arranque lento, mucho consumo de disco y memoria.
- Contenedor → kernel compartido, arranca en segundos, muy ligero.
3. Conocer las piezas: contenedor, imagen, Dockerfile y DockerHub
Para entender Docker hay que conocer sus cuatro piezas básicas:
- Dockerfile → archivo de texto con las instrucciones necesarias para crear una imagen.
- Imagen → plantilla de solo lectura que incluye todos los elementos que hacen falta para levantar un contenedor.
- Contenedor → una instancia de la imagen puesta en ejecución.
- DockerHub → repositorio de imágenes donde se publican y se descargan.
Una forma sencilla de recordarlo: el Dockerfile es la receta, la imagen es el plato preparado y congelado, y el contenedor es el plato que estás sirviendo y comiendo ahora.
4. Instalar Docker en Linux
Para instalar Docker de forma rápida y oficial en la mayoría de distribuciones Linux:
curl -fsSL https://get.docker.com | sh
Una vez instalado, comprobamos que todo ha ido bien:
docker --version
docker info
Nota: si vamos a usar Docker con un usuario concreto (sin sudo), lo añadimos al grupo docker. Hay que cerrar y volver a abrir la sesión para que el cambio surta efecto:
sudo usermod -aG docker $USER
Y lanzamos una imagen de prueba:
docker run hello-world
5. Primeros pasos
Cuando ya tenemos instalado Docker, lo primero que nos interesa es lanzar contenedores. Podemos empezar buscando una imagen:
docker search nginx
Esto nos devuelve una lista de imágenes disponibles en DockerHub. Es solo una consulta: no podemos ejecutar un resultado directamente. Para ejecutar la imagen:
docker run nginx
Si no tenemos la imagen en local, Docker hace un pull, la descarga y ejecuta el contenedor en primer plano. Se queda escuchando, pero no podremos escribir nada ni acceder desde el navegador, porque no hemos publicado ningún puerto.
Si abrimos otra terminal y lanzamos docker ps, veremos que está corriendo.
Para poder verlo desde el navegador y dejarlo en segundo plano:
docker run -d --name TEST -p 8080:80 nginx
Con -d lo mandamos a segundo plano y con -p 8080:80 hacemos que el puerto 8080 del host apunte al puerto 80 del contenedor, de modo que podamos acceder desde fuera. El orden es siempre host:contenedor.
Iremos viendo todos los parámetros, pero los principales son:
| Parámetro | Para qué sirve |
|---|---|
-d (detached) | Ejecuta el contenedor en segundo plano. |
--name | Le da un nombre al contenedor. No se pueden repetir. |
-p | Mapea un puerto del host a un puerto del contenedor. |
-v | Mapea un volumen al contenedor. |
-e / --env-file | Define una variable o un archivo de variables. |
--rm | Elimina el contenedor al pararlo. |
--restart | Define si el contenedor se reinicia solo tras un fallo o un reinicio. |
De las políticas de reinicio de --restart, las más usadas son:
| Opción | Comportamiento |
|---|---|
no | Si el contenedor se apaga, se queda apagado. |
always | Siempre intenta encenderlo, aunque lo apagues tú. |
unless-stopped | Se reinicia si falla o si reinicias el servidor, pero si tú lo apagas se queda quieto. |
on-failure | Solo vuelve a encenderse si el contenedor termina con un código de error, no si sale limpio. |
Al tener la terminal libre, podemos comprobar el estado con docker ps, que muestra los contenedores en ejecución; si le añadimos -a vemos también los que están parados:
docker ps
docker ps -a
Para pararlo y borrarlo, y para limpiar de golpe todos los contenedores parados:
docker stop TEST
docker rm TEST # también vale el CONTAINER ID
docker container prune # borra todos los contenedores parados
Por último, es interesante conocer los logs de nuestro contenedor. El -f (follow) nos deja verlos en vivo, según se van generando:
docker logs -f ID-CONTENEDOR
6. Ciclo de vida de un contenedor
Para entender el ciclo de vida hay que distinguir primero entre contenedores de aplicaciones (web, base de datos…) y contenedores con un sistema Linux mínimo (alpine, ubuntu).
Un contenedor vive mientras vive su proceso principal. Por eso, si lanzamos docker run alpine a secas, no encuentra nada que hacer y finaliza al instante. Para que no muera hay que darle un proceso, y lo indicamos al final del comando:
El problema es que ejecuta ese comando y muere. Podemos usar un comando que lo mantenga "vivo", como sleep (sin olvidar el -d para lanzarlo en segundo plano), y entrar después al contenedor si lo necesitamos:
docker run -d --name PRUEBA alpine sleep 3600
docker exec -it PRUEBA sh
Si lo que queremos es lanzar el Linux mínimo directamente en modo interactivo, lo hacemos todo en un solo comando (siendo sh la shell desde la que ejecutaremos los comandos):
docker run -it alpine sh
Tenemos que entender claramente la diferencia: run crea un contenedor nuevo y exec entra en uno que ya está en marcha.
Por último, recordemos pararlo y borrarlo. También podemos volver a arrancar un contenedor parado, o forzar su parada:
docker stop NAME
docker rm NAME
docker start NAME # arranca uno parado
docker kill NAME # fuerza la parada
7. Imágenes
Como hemos visto, una imagen es un paquete de solo lectura que contiene todo lo necesario para ejecutar un contenedor. Lo principal es que una imagen no se puede modificar: para cambiar algo se crea una imagen nueva.
Además, una imagen está formada por capas: cada instrucción de su construcción (que veremos en el Dockerfile) genera una capa apilada sobre la anterior. Como esas capas se reutilizan entre imágenes, se reduce el tamaño y el tiempo de construcción.
Cuando descargamos una imagen podemos listarla y consultar sus pasos de construcción:
docker pull mariadb
docker images
docker history mariadb
Y como vimos antes, podemos buscar imágenes desde la terminal con docker search mariadb.
Para borrar imágenes tenemos docker rmi, o docker image prune para eliminar las que nadie usa. Si la imagen está en uso no se borrará, a no ser que usemos -f. Para borrar también las que no están etiquetadas —el caso más habitual— añadimos -a:
docker rmi mariadb
docker image prune
docker image prune -a
8. Primer Dockerfile
Un Dockerfile es un fichero de texto con las instrucciones para construir una imagen: es la receta. El fichero se llamará siempre Dockerfile. Su sintaxis principal es:
| Instrucción | Qué hace |
|---|---|
FROM | Imagen base (siempre la primera línea). |
RUN | Ejecuta comandos durante la construcción. |
COPY | Copia ficheros de tu máquina a la imagen. |
WORKDIR | Fija el directorio de trabajo (el cd de la imagen). |
EXPOSE | Informa de qué puerto usa la aplicación. |
ENV | Define una variable de entorno disponible dentro del contenedor. |
ARG | Pasa argumentos a nuestro proceso de construcción. |
CMD | El comando que arranca al lanzar el contenedor. |
ENTRYPOINT | Como CMD, pero fijo; el CMD pasa a ser sus argumentos. |
Un ejemplo mínimo:
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
Y un ejemplo algo más completo:
FROM opensuse/leap:15.6
RUN zypper -n install nginx
COPY index.html /srv/www/htdocs/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
En este Dockerfile partimos de openSUSE Leap (FROM), instalamos nginx durante la construcción con zypper (RUN) y copiamos el index.html a donde nginx sirve las páginas (COPY). EXPOSE solo informa del puerto 80 —para publicarlo sigue haciendo falta el -p—, y CMD arranca nginx con daemon off; en primer plano, para que el proceso no muera y el contenedor siga vivo.
Una vez escrito el Dockerfile, construimos la imagen y la lanzamos. El punto final del build indica el contexto, es decir, el directorio donde está el Dockerfile:
docker build -t NAME .
docker run -d --name TEST -p 8080:80 NAME
Orden de las instrucciones
Docker construye por capas y las cachea. Poner arriba lo que menos cambia y abajo lo que más cambia hace que las reconstrucciones sean mucho más rápidas.
Diferencias entre ARG y ENV
ARG existe solo durante la construcción de la imagen; ENV define una variable que sigue viva dentro del contenedor ya en ejecución.
Diferencias entre CMD y ENTRYPOINT
CMD define un comando por defecto que puedes sustituir al lanzar el contenedor; ENTRYPOINT fija el comando, y lo que pases pasa a ser sus argumentos. Con un ejemplo se ve claro:
FROM alpine
CMD ["echo", "hola"]
Al lanzarlo:
docker run miimagen
→ hola
docker run miimagen echo adios
→ adios
Es decir, lo que escribimos detrás de la imagen sustituye al CMD. Veamos ahora el mismo caso con ENTRYPOINT:
FROM alpine
ENTRYPOINT ["echo"]
Al lanzarlo:
docker run miimagen hola
→ hola
docker run miimagen adios
→ adios
Aquí echo está fijo: no lo puedes cambiar al lanzar. Lo que escribes detrás (hola, adios) no lo sustituye, sino que se le añade como argumento.