Descubriendo las bases de Docker

por | Oct 30, 2024

Hasta ahora.

fundamentos de la virtualización
Contenedores y el motor de containerización

Y aquí es donde entra Docker, el motor que permite crear y gestionar estos contenedores de manera eficiente. Con Docker, puedes crear aplicaciones portátiles, escalables y fáciles de mantener.

Ahora que ya tienes una idea general de lo que es Docker, es hora de profundizar en dos de los conceptos más importantes: imágenes y contenedores.
Pensemos en una imagen de Docker como una receta detallada. Esta receta (imagen) no solo contiene las instrucciones (Dockerfile) para un plato específico, como una pizza (contenedor), sino que también incluye todos los ingredientes pre-medidos (códigos, configuraciones y las dependencias necesarias) y herramientas necesarias.

La imágenes de Docker funcionan como una receta de comida, Los Dockerfiles son las instrucciones
Las imágenes de Docker funcionan como una receta:
🗒️los Dockerfiles son las instrucciones que indican paso a paso cómo crear un contenedor🗒️
los códigos, las configuraciones y las dependencias son los ingredientes en la imagen de Docker.
🥚 En nuestro ejemplo, los ingredientes son el código, las configuraciones y las dependencias necesarias 🧀

Pero tener una receta no es suficiente. Necesitas convertir esa receta en una pizza real, ¿verdad?
Aquí es donde entran los contenedores.

Para crear una imagen es necesario utilizar un archivo especial llamado Dockerfile (en nuestra analogía se trata de las instrucciones para una pizza), que indica paso a paso qué componentes necesita tu aplicación y cómo deben instalarse.

Por ejemplo, si quieres construir una aplicación con Node.js, tu Dockerfile podría incluir los siguientes pasos:

  1. Empieza con una imagen base (un sistema operativo ligero como Alpine Linux).
  2. Añade Node.js.
  3. Copia tu código dentro de la imagen.
  4. Especifica cómo debe ejecutarse la aplicación.

Una vez que tienes Dockerfile, Docker se encarga de construir la imagen. Esta imagen luego se puede compartir y reutilizar.

Un contenedor es una instancia de tu imagen. Para continuar con nuestra analogía de la pizza: una vez que tienes la receta (imagen), podrías hacer tantas pizzas como quieras (contenedores), todas basadas en la misma receta.
La magia de Docker es que puedes tener múltiples contenedores corriendo simultáneamente, todos basados en la misma imagen, pero cada uno funcionando de manera independiente. Esto te permite escalar aplicaciones fácilmente o tener varias versiones corriendo sin problemas.

Con la imagen, se pueden crear los contenedores que desees
😋 Luego de crear tu imagen con los ingredientes e instrucciones, podrás generar todos los contenedores que desees 😋

Aquí es donde Docker muestra todo su poder. Además de manejar imágenes y contenedores, Docker también gestiona volúmenes y redes, dos elementos que permiten que las aplicaciones se comuniquen y almacenen datos de forma persistente.

  • Los volúmenes funcionan como bases de datos donde tu aplicación guarda información, conservando los datos generados y utilizados por los contenedores Docker. 
  • Las redes en Docker son como carreteras que conectan y comunican diferentes contenedores entre sí o con el mundo exterior. 

Hasta aquí deberías tener una idea bastante clara de cómo funcionan los componentes básicos de Docker.

Para que Docker funcione correctamente, se basa en varios componentes clave. Cada uno tiene una función específica en el proceso de creación y gestión de contenedores.

Los componentes de Docker: client, daemon, containerd, runc y contenedor

El Cliente de Docker es como el control remoto de tu videojuego. Es la interfaz que utilizas para interactuar con Docker. A través de este, envías comandos para crear, gestionar y destruir contenedores. Cuando ejecutas un comando en la línea de comandos, es el cliente el que se encarga de comunicarse con el Daemon de Docker para que realice las acciones solicitadas.

El Daemon de Docker, conocido como dockerd, es el cerebro detrás de Docker. Se encarga de ejecutar los comandos que le envía el Cliente y maneja todo lo relacionado con la creación, ejecución y gestión de contenedores. Es el proceso en segundo plano que hace que las cosas sucedan.

containerd es un nivel intermedio en la arquitectura de Docker. Se encarga de gestionar el ciclo de vida de los contenedores, desde su creación hasta su eliminación. Administra aspectos como el inicio, la detención y la eliminación de contenedores, y también se encarga de manejar volúmenes, redes y otros recursos asociados.

runc es el componente más cercano al sistema operativo. Es un runtime de bajo nivel que se encarga de ejecutar los contenedores. Interactúa directamente con el sistema operativo para iniciar y detener los contenedores.

Ahora que entiendes los componentes básicos, es hora de instalar Docker. Dependiendo de tu sistema operativo, el proceso varía un poco. Aquí te damos una guía rápida para Linux, Windows y Mac.

Para los usuarios de Linux, instalaremos Docker en Ubuntu. Sigue estos pasos para que tu sistema esté listo para correr contenedores:

  1. Actualiza los paquetes existentes:
sudo apt-get update

2. Instala las dependencias necesarias:

sudo apt-get install 
    apt-transport-https 
    ca-certificates 
    curl 
    gnupg 
    lsb-release

3. Añade la clave GPG del repositorio de Docker:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

4. Añade el repositorio de Docker:

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu 
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

5. Actualiza los paquetes nuevamente:

sudo apt-get update

6. Instala Docker:

sudo apt-get install docker-ce docker-ce-cli containerd.io

7. Verifica la instalación:

sudo docker -v

Si deseas ejecutar Docker siendo root:

sudo groupadd docker

Y agregar usuario al grupo de Docker para ejecutar Docker sin ser root

sudo usermod -aG docker $USER 

Instalación en Windows.

Para Windows, el proceso es mucho más simple. Solo sigue estos pasos:

  1. Descarga Docker Desktop desde el sitio web oficial.
  2. Instala el software siguiendo las instrucciones del instalador.
  3. Inicia Docker Desktop y asegúrate de que esté funcionando correctamente.

Instalación en Mac.

Al igual que en Windows, la instalación en Mac es bastante directa:

  1. Descarga Docker Desktop desde el sitio web oficial.
  2. Abre el archivo descargado y arrastra el icono de Docker a tu carpeta de Aplicaciones.
  3. Inicia Docker Desktop y verifica que esté funcionando.

¡Prueba Docker sin instalación!

Si no quieres instalar Docker de inmediato, puedes usar Play with Docker. Este sitio te proporciona un entorno de prueba en línea durante 4 horas para que puedas experimentar con Docker sin necesidad de instalar nada en tu máquina.

Primeros pasos con Docker: Crear y ejecutar contenedores

Ahora que tienes Docker instalado y funcionando, es el momento de adentrarse en los conceptos que hacen que Docker sea tan poderoso. Vamos a desglosar imágenes, contenedores y volúmenes de una manera sencilla, usando analogías para que todo quede claro.

Imágenes.

Las imágenes de Docker son como las plantillas en un juego: contienen todo lo necesario para ejecutar una aplicación, incluyendo el código, las dependencias, la configuración y las variables de entorno. Si estás familiarizado con las máquinas virtuales, una imagen es similar a un template o una snapshot. En términos de programación, podríamos considerar una imagen como una clase, mientras que un contenedor sería una instancia de esa clase.

Una imagen permite crear un contenedor

¿Cómo se recuperan las imágenes?

Cuando instalas Docker, no hay imágenes predefinidas en tu máquina. Puedes verificar esto con el comando:

docker image ls

Si no hay imágenes, obtendrás una lista vacía como esta:

REPOSITORY       TAG       IMAGE       ID       CREATED       SIZE

Para recuperar una imagen, usas el comando de pull:

docker image pull repository:tag
«OR
docker  pull node: latest»

Por ejemplo, para descargar la última versión de Node.js, usarías:

docker image pull node:latest

NOTA: Docker Hub es un marketplace en el que puedes encontrar imágenes oficiales, como la de node:latest, junto con imágenes creadas por la comunidad o por empresas. Además, puedes almacenar tus propias imágenes a Docker Hub para compartirlas o reutilizarlas en otros proyectos.

Verás mensajes de progreso mientras se descargan varios elementos, por ejemplo.

latest: Pulling from library/node
d960726af2be: Pull complete
e8d62473a22d: Pull complete
8962bc0fad55: Pull complete
65d943ee54c1: Pull complete
532f6f723709: Pull complete
f8463f32765b: Pull complete
39c1cd906e85: Pull complete
9b89015c57b4: Pull complete
6a93d724110f: Pull complete
Digest:sha256:ddc2ebfc3759299afb2085bba7648416e016b326389323a257c4f8d1da097c9c
Status: Downloaded newer image for node:latest
docker.io/library/node:latest

Una vez descargada, puedes verificar la imagen con:

docker image ls
REPOSITORY                                      TAG                  IMAGE ID       CREATED        SIZE
node                                            latest               7493e35c7ffa   13 days ago    908MB

¿Dónde se almacenan estas imágenes? 

Las imágenes están almacenadas en lo que se llama image registries. Un registro es un repositorio donde se almacenan y distribuyen las imágenes de Docker, similar a npm para JavaScript o Packagist para PHP. Existen registros tanto públicos como privados, lo que permite almacenar imágenes de acceso abierto o restringido, según las necesidades. El registro por defecto es Docker Hub, un registro público, pero también hay otros disponibles que pueden ser configurados como privados para un control más seguro y personalizado de las imágenes.

Puedes ver el registro utilizado por Docker con:

docker info

Esto te mostrará información sobre el registro actual, como:

Server:
...
 Registry: https://index.docker.io/v1/
...

Entender los repositorios y Tags: 

Un registro puede contener varios repositorios, cada uno con diferentes versiones (tags) de las imágenes. Por ejemplo, en el Docker Hub, la imagen node puede tener diferentes tags, como latest o 16. Si descargas una imagen con un tag específico, como node:16, y luego listas las imágenes, verás que los IDs de las imágenes pueden ser iguales, indicando que en esencia es la misma imagen.

Las Capas (Layers) de las Imágenes:

Cada imagen Docker está compuesta de varias capas, y cada una es de solo lectura. Estas capas se crean a partir de las instrucciones en un Dockerfile. Cada línea en el Dockerfile (como FROM, RUN, COPY, etc.) genera una nueva capa.

Docker utiliza un sistema de archivos llamado Union File System para fusionar estas capas y presentar una vista unificada. Una ventaja importante de este sistema es que las capas pueden ser compartidas entre diferentes imágenes, lo que optimiza el almacenamiento y la eficiencia.

Por ejemplo, la imagen oficial de Node está compuesta de múltiples capas, cada una creada a partir de una instrucción específica en su Dockerfile.

Las instrucciones, como FROM, COPY, RUN, CMD, etc, crean estas capas. Cuando se ejecuta una de estas instrucciones, Docker genera una nueva capa y la integra en la imagen.

Comandos Principales para Imágenes:

Recuperar una Imagen:

docker image pull [OPTIONS] NAME[:TAG|@DIGEST]

Ejemplo para descargar una versión específica:

docker image pull node:16-alpine3.11

Listar Imágenes:

docker image ls [OPTIONS] [REPOSITORY[:TAG]]
  • Algunas opciones útiles:
    • --all, -a: Lista todas las imágenes.
    • --filter: Aplica un filtro para mostrar imágenes específicas, como las sin tags.

Por ejemplo, para mostrar imágenes sin tags:

docker image ls --filter dangling=true

Eliminar Imágenes:

docker image rm [OPTIONS] IMAGE [IMAGE...]

Para eliminar imágenes no etiquetadas:

docker image prune

Buscar Imágenes:

docker search TERM

Por ejemplo, para buscar un imagen en Redis:

docker search redis

Contenedores: La Instancia en Acción

Un contenedor es simplemente un entorno de ejecución de procesos aislado del resto del sistema, por lo que no es posible que un contenedor acceda a los recursos de otro contenedor o de la máquina host, al menos si no se le ha autorizado explícitamente.

Para iniciar tu primer contenedor, comienza con algo simple. Utiliza la siguiente orden para ejecutar un contenedor de prueba que te saludará con un mensaje amigable:

docker run hello-world

¿Qué sucederá aquí? Docker está realizando estos pasos:

  1. El cliente Docker envía una orden al demonio Docker.
  2. El demonio Docker descarga la imagen hello-world desde Docker Hub (si aún no está localmente).
  3. El demonio de docker (dockerd) crea un nuevo contenedor a partir de esta imagen y ejecuta el programa dentro.
  4. Finalmente, el demonio transmite el mensaje al cliente Docker y lo muestra en la terminal.

¡Felicidades! Has lanzado tu primer contenedor. Pero no te detengas aquí.

Prueba con algo más ambicioso como un contenedor Ubuntu:

docker run -it ubuntu bash

Aquí usamos las opciones -it para activar el modo interactivo y acceder a la terminal del contenedor. Verás un prompt de Ubuntu en el contenedor, ¡ahora puedes explorar y ejecutar comandos!

Trabajando con Contenedores

Ya que estás dentro del contenedor, ejecuta comandos como ls -al para listar los archivos ocultos y todos los archivos/directorios que hay. Y ps -l para ver los procesos en ejecución. Verás que el único programa ejecutándose es el bash que lanzaste, y una vez que termines con él, el contenedor se detendrá.

Para salir del contenedor, simplemente usa el comando exit y regresarás a tu máquina anfitriona

Desplegando Aplicaciones

Ahora, intentemos algo diferente. Lanza un contenedor con Apache en segundo plano usando:

docker run -d --name test-apache2 -p 8080:80 httpd

Aquí usamos varias opciones nuevas:

  • -d para ejecutar el contenedor en segundo plano.
  • --name para darle un nombre al contenedor.
  • -p para mapear el puerto 8080 del anfitrión al puerto 80 del contenedor.

Visita http://localhost:8080/ para verificar que Apache está funcionando.

Puedes conectarte al contenedor en ejecución con:

docker exec -ti test-apache2 bash

Desde aquí, puedes modificar archivos y ver los cambios reflejados en tiempo real en tu navegador.

Gestionando el Ciclo de Vida de los Contenedores

Los contenedores tienen un ciclo de vida que incluye inicio, detención y eliminación.

Para detener un contenedor, usa:

docker container stop test-apache2
  • Si verificamos los contenedores en ejecución con el comando docker container ls (también podemos usar docker ps)
docker container ls
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
  • Observaremos que no hay contenedores en ejecución. Para ver todos los contenedores, incluso los detenidos, debemos agregar la opción -a (o --all)

Para eliminar un contenedor detenido, usa:

docker container rm test-apache2
  • Verifiquemos  que se detenido correctamente
docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Recuerda que un contenedor debe estar detenido antes de ser eliminado. Puedes también pausar y reanudar contenedores según sea necesario.

El ciclo de vida de un contenedor

Políticas de Reinicio

Puedes configurar cómo se reinician tus contenedores usando la opción --restart. Las opciones disponibles incluyen:

  • no: No reinicia los contenedores automáticamente.
  • always: Reiniciar siempre que se detenga.
  • unless-stopped: Reiniciar siempre que se detenga, excepto si se detiene manualmente.
  • on-failure: Reiniciar solo si el contenedor falla.

Ejemplo de uso con Ubuntu:

docker run -it --restart always ubuntu bash

Esto hará que el contenedor se reinicie automáticamente si se detiene.

Comandos Clave para Contenedores

Aquí tienes algunos comandos esenciales para manejar contenedores:

  • Iniciar un contenedor:
docker container run [OPTIONS] IMAGE [COMMAND] [ARG...]

Opciones útiles incluyen -d para ejecución en segundo plano y --name para asignar un nombre al contenedor.

  • Detener un contenedor:
docker container stop [OPTIONS] CONTAINER [CONTAINER...]

Este comando envía un SIGTERM al proceso principal del contenedor y, si es necesario, SIGKILL después de 10 segundos.

  • Eliminar contenedores:
docker container rm [OPTIONS] CONTAINER [CONTAINER...]

Para eliminar un contenedor, primero debe estar detenido. Usa -f para forzar la eliminación de un contenedor en ejecución.

  • Ejecutar el comando en un contenedor:
docker container exec [OPTIONS] CONTAINER COMMAND [ARG...]

Esto te permite ejecutar comandos directamente en un contenedor en funcionamiento.

  • Algunas opciones útiles son:

Modo interactivo: --interactive , -i

Abrir una terminal: --ty , -t

  • Listar contenedores:
docker container ls [OPTIONS]

Añade -a (o --all) para ver todos los contenedores, incluidos los que están detenidos.

La persistencia de datos en Docker

Una de las grandes dificultades con Docker es cómo manejar los datos dentro de los contenedores. Cuando trabajas con imágenes, estas están formadas por varias capas en modo de solo lectura. Esto es perfecto para crear entornos reproducibles y consistentes, pero ¿qué pasa con los datos que quieres conservar cuando terminas el trabajo? Aquí es donde entra el truco.

Cuando creas un contenedor, Docker añade una capa adicional en modo lectura/escritura.

Cada contenedor dispone de una capa en modo lectura/escritura

El problema es que esta capa es efímera: cuando el contenedor se elimina, esta capa y todos los datos que contiene también se pierden. Afortunadamente, Docker ofrece dos soluciones para resolver este problema:

  • los montajes vinculados (bind mounts); 
  • los volúmenes nombrados.

Montajes vinculados (Bind Mounts)

La primera solución es usar los montajes vinculados, que es una forma bastante sencilla de vincular un directorio en tu máquina host a un directorio en el contenedor.

Solo necesitas usar la opción -v en el comando docker run, especificando el directorio en la máquina host y el directorio en el contenedor.

Por ejemplo, si tienes un contenedor Apache y quieres vincular el directorio /home/arkerone/docker/apache-test en tu máquina host con el directorio /usr/local/apache2/htdocs en el contenedor, el comando sería:

docker run -d --name test-apache2 -p 8080:80 -v /home/arkerone/docker/apache-test:/usr/local/apache2/htdocs httpd

Esto crea una vinculación directa entre tu máquina host y el contenedor.

Puedes modificar los archivos en el directorio de tu máquina host y esos cambios se reflejarán en el contenedor.

Sin embargo, ten en cuenta que los montajes vinculados dependen de la estructura del sistema de archivos de la máquina host, lo que puede causar problemas si accidentalmente modificas esa estructura. Además, pueden presentar riesgos de seguridad al permitir que un contenedor modifique la estructura del sistema de archivos del host.

Volúmenes nombrados

Para superar las limitaciones de los montajes vinculados, Docker ofrece volúmenes nombrados. Estos son gestionados completamente por Docker, independientemente de la estructura de archivos de la máquina host, lo que los hace una opción más robusta y segura.

Para crear un volumen nombrado, usa el siguiente comando:

docker volume create test-apache2-vol

Luego, puedes montar este volumen en un contenedor como lo harías con un montaje vinculado, pero sin preocuparte por la estructura del sistema de archivos de la máquina host. El comando sería:

docker run -dit --name test-apache2-1 -p 8080:80 -v test-apache2-vol:/usr/local/apache2/htdocs httpd

Si deseas usar la opción --mount, también puedes hacerlo, pero asegúrate de que el volumen haya sido creado previamente:

docker run -dit --name test-apache2-1 -p 8080:80 --mount type=volume,src=test-apache2-vol,dst=/usr/local/apache2/htdocs httpd

Los volúmenes nombrados permiten que varios contenedores compartan los mismos datos. Por ejemplo, si creas un segundo contenedor que también usa el volumen test-apache2-vol, ambos contendores tendrán acceso a los mismos datos.

¿Montaje vinculado o volumen nombrado?

Entonces, ¿cuándo usar cada uno?

Los montajes vinculados son útiles en el desarrollo, donde necesitas ver rápidamente los cambios en tu código sin tener que reconstruir la imagen del contenedor. Sin embargo, para la mayoría de las situaciones de producción, los volúmenes nombrados son la opción recomendada. Son más seguros y gestionados por Docker, lo que evita los problemas asociados con la estructura de archivos del host.

Comandos principales para manejar volúmenes

Para trabajar con volúmenes en Docker, aquí tienes algunos comandos útiles:

  • Crear un volumen:
docker volume create [OPTIONS] [VOLUME]
  • Listar volúmenes:
docker volume ls [OPTIONS]
  • Eliminar un volumen:
docker volume rm [OPTIONS] VOLUME [VOLUME...]
  • Eliminar volúmenes no utilizados:
docker volume prune [OPTIONS]

Redes en Docker

La comunicación entre contenedores es otro aspecto esencial en Docker. Los contenedores necesitan una capa de red para comunicarse entre sí o con el mundo exterior. Docker proporciona varios tipos de redes, pero en este artículo nos centraremos en el tipo bridge, que es el más común.

Para crear una red bridge:

docker network create -d bridge localnetwork

Esto crea una red llamada localnetwork. Luego, puedes conectar contenedores a esta red y permitir que se comuniquen entre sí. Por ejemplo:

docker container run -it --name container-1 --network localnetwork alpine sh
docker container run -it --name container-2 --network localnetwork alpine sh

Desde el primer contenedor, puedes hacer ping al segundo contenedor para verificar la conexión:

/ # ping container-2

Comandos principales para manejar redes

Aquí tienes algunos comandos útiles para trabajar con redes en Docker:

Crear una red:

docker network create [OPTIONS] NETWORK
  • Listar redes:
docker network ls [OPTIONS]
  • Eliminar una red:
docker network rm NETWORK [NETWORK...]
  • Eliminar redes no utilizadas:
docker network prune [OPTIONS]
  • Conectar un contenedor a una red:
docker network connect [OPTIONS] NETWORK CONTAINER
  • Desconectar un contenedor de una red:
docker network disconnect [OPTIONS] NETWORK CONTAINER

Conclusión

Docker puede parecer un desafío al principio, pero con la comprensión de las bases, te acercarás a dominar esta potente herramienta. Si bien este artículo cubre lo básico, hay mucho más por explorar.

¡Continúa practicando y experimentando con Docker, y pronto te sentirás como un experto en esta herramienta esencial!