Trabajando con Docker: Instalación, configuración y uso

February 23rd, 2022

Wikipedia define Docker de la siguiente forma:

Docker es un proyecto de código abierto que automatiza el despliegue de aplicaciones dentro de contenedores de software, proporcionando una capa adicional de abstracción y automatización de virtualización de aplicaciones en múltiples sistemas operativos.1​ Docker utiliza características de aislamiento de recursos del kernel Linux, tales como cgroups y espacios de nombres (namespaces) para permitir que "contenedores" independientes se ejecuten dentro de una sola instancia de Linux, evitando la sobrecarga de iniciar y mantener máquinas virtuales.

De acuerdo con la firma analista de la industria 451 Research, ‘Docker es una herramienta que puede empaquetar una aplicación y sus dependencias en un contenedor virtual que se puede ejecutar en cualquier servidor Linux. Esto ayuda a permitir la flexibilidad y portabilidad en donde la aplicación se puede ejecutar, ya sea en las instalaciones físicas, la nube pública, nube privada, etc.

En los últimos años, Docker se ha revelado como una herramienta fundamental tanto para administradores de sistemas como para desarrolladores de software. Durante el desarrollo y puesta en producción de una aplicación, uno de los conflictos típicos a los que se enfrentan ambos perfiles viene dado por la heterogeneidad, tanto en infraestructura como en dependencias, derivados de la puesta en marcha de los distintos entornos -desarrollo, preproducción y producción- típicos de cualquier proyecto informático.

El empaquetado en contenedores aislados permite realizar una abstracción de nuestras aplicaciones respecto al sistema operativo instalado en el servidor, ya que estos contenedores se generan con todas las dependencias que permiten la ejecución del software. Efectivamente, esto quiere decir que podremos llevarnos estos contenedores de un entorno a otro sin mayor inconveniente. Esto reduce en gran medida los típicos problemas en los que, por algún motivo, una aplicación que puede ejecutar correctamente en un entorno determinado, no lo haga igual en otro – en este punto, siempre se hace referencia al clásico: “en mi máquina funciona”-, ya que todo lo que la aplicación necesita se encuentra empaquetado dentro del contenedor.

Una de las diferencias más notables respecto al hacer uso de máquinas virtuales pasa por una simplificación de las dependencias e infraestructura necesarias para ejecutar aplicaciones. Si hacemos uso de máquinas virtuales a través de un hipervisor, nos veremos en la necesidad de instalar un SO y las correspondientes dependencias en cada una de ellas, mientras que Docker nos permite operar todos nuestros contenedores sobre un único SO base, común y compartido por todos ellos, y encapsular solo las dependencias necesarias en cada uno de estos contenedores. Esta imagen extraída de la web oficial de Docker resume esta idea:

docker1

Las ventajas no terminan aquí, y es que Docker se muestra como una tecnología más ágil y flexible que la que nos puedan ofrecer las máquinas virtuales, con todas las ventajas, tanto en ahorro de tiempo -un contenedor tiene un menor mantenimiento respecto a cualquier SO- como de aumento de potencial, que esto supone. Tampoco debemos olvidarnos del ahorro económico que puede suponer la no utilización de licencias en el caso de que hagamos uso de distribuciones de pago.

Componentes principales de Docker

Este gráfico extraído de la documentación oficial de Docker nos ayuda a realizar una aproximación a los principales componentes de la herramienta y su rol dentro de la infraestructura:

docker2

Docker hace uso de una arquitectura cliente-servidor, es decir, el cliente lanza peticiones al servicio (deamon), que a su vez es el responsable de generar los recursos solicitados. No obstante, es posible que cliente y demonio se encuentren corriendo en la misma máquina.

Para poder levantar un contenedor, necesitamos hacer uso de una imagen. Una imagen es una plantilla de lectura que sirve para crear contenedores con las características deseadas, como puede ser cualquier SO con una aplicación determinada corriendo dentro. Estas imágenes se almacenan en repositorios, que pueden ser públicos o privados, y donde se puede llevar un control de cambios y versiones. Algo a tener muy en cuenta es que los cambios realizados en el contenedor no se reflejan en la imagen, ya que esta se utiliza únicamente como lanzadera, por lo que, si queremos aplicar algún cambio, debemos realizarlo tomando la base de la imagen de origen, añadiendo los cambios pertinentes y generando una nueva imagen; o bien, generando una imagen basada en un contenedor con los cambios ya aplicados.

Llegados a este punto, conviene hacer hincapié en la idea de que los contenedores se consideran recursos efímeros, es decir, podremos prescindir de ellos de una manera sencilla, ya que son fácilmente replicables y, de hecho, forma parte de la filosofía de esta tecnología. Cuando hacemos un cambio determinado en nuestra aplicación, no actualizamos el contenedor, si no que levantamos uno nuevo con los cambios necesarios aplicados en una nueva imagen, mientras que descartamos el obsoleto.

Pero, entonces, si los contenedores son volátiles y fácilmente desechables, ¿qué ocurre con los datos contenidos en ellos cuando uno de estos contenedores muere o es eliminado? Pues, efectivamente, esa información se pierde, pero solo si no le ponemos remedio antes. Para solventar este problema, Docker puede hacer uso de volúmenes destinados a almacenar datos de forma persistente. Estos volúmenes están conectados al contenedor, pero no forman parte de él; de esta forma, cuando el contenedor falla o es eliminado o reiniciado, la información contenida en el volumen no se pierde, por lo que podremos conectar el nuevo contenedor a este volumen y poder seguir accediendo de nuevo a estos datos.

Instalando Docker

Docker puede instalarse sobre SSOO Windows, MACos y Linux. En nuestro caso, haremos un resumen rápido de cómo llevar a cabo la instalación sencilla sobre Ubuntu Server 20.04 atendiendo a la documentación oficial que nos facilita Docker en su sitio web oficial.

Como primer paso, debemos asegurarnos de que nuestra máquina no cuenta con ninguna versión obsoleta de Docker, por lo que debemos cerciorarnos de que ninguno de estos paquetes se encuentre instalado en nuestro SO:

$ sudo apt-get remove docker docker-engine docker.io containerd runc

La instalación la vamos a realizar haciendo uso del repositorio de Docker. Antes de comenzar la instalación, nos aseguramos de que nuestros repositorios se encuentran actualizados:

$ sudo apt-get update

Ahora vamos a adecuar apt para hacer uso de los repositorios HTTPS. Para ello, ejecutamos lo siguiente:

$ sudo apt-get install \     ca-certificates \     curl \     gnupg \     lsb-release

A continuación, añadimos la GPG Key oficial de Docker a nuestro sistema:

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

Y añadimos el repositorio:

$ echo \  "deb [arch=$(dpkg --print-architecture) 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

Una vez añadido, ya podemos llevar a cabo la instalación de la última versión disponible de Docker Engine:

$ sudo apt-get update

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

Cuando finalice la instalación, podremos comprobar si todo el proceso de instalación ha ido como debe. Para ello, ejecutaremos un contenedor de testeo:

$ sudo docker run hello-world

A través de esta ejecución, estaremos descargando y ejecutando un contenedor con una imagen de testeo que mostrará un mensaje validando la instalación, cerrándose a continuación:

docker3

En el mensaje de salida se nos detalla el flujo de procesos encargados de que el cliente conecte con el servicio de Docker y cree un contenedor con la imagen de testeo, lo que representa en sí mismo la comprobación de que el proceso de instalación se ha completado con éxito.

Una vez ejecutados estos sencillos pasos, tendremos el servicio de Docker corriendo y operativo en nuestro servidor.

Creando nuestro primer contenedor

Vamos a generar nuestro primer contenedor haciendo uso de una imagen pública de Nginx. Podemos realizar búsquedas a través del comando docker search:

$ docker search nginx | head -10

Limitamos el resultado para que no llenar la salida de texto, y podemos ver que se nos ofrecen diferentes resultados basados en nuestra búsqueda:

docker4

Descargamos la imagen oficial de nginx. Para ello, debemos fijarnos en el nombre de la imagen que aparece en la búsqueda, en este caso, simplemente nginx:

$ docker pull docker.io/nginx

Y comenzará el proceso de descarga:

docker5

La imagen superior es una buena muestra de cómo Docker genera imágenes conformadas por capas. De esta forma, cuando realicemos algún cambio en nuestra imagen, solo se verán afectadas algunas de ellas, dependiendo de la cantidad de cambios, y no la totalidad, lo que agiliza el proceso de actualización.

Para listar las imágenes descargadas, podemos ejecutar:

$ docker images

docker6

Ahora, vamos a ejecutar el contenedor con nginx:

$ docker run -it --rm -d -p 8080:80 --name web nginx

Y a listar todos los procesos docker en ejecución:

$ docker ps

docker7

Con este comando, estaremos generando un contenedor al que podremos acceder a través del puerto 8080 de nuestra máquina virtual. Para validar la ejecución, podemos ejecutar un curl en nuestro localhost al puerto 8080:

docker8

Para ver el resultado en un navegador de nuestra máquina local, introducimos la IP de la máquina virtual apuntando al puerto 8080:

docker9

¡Ya tenemos nuestro servidor web ejecutando en un contenedor Docker!

Vamos a realizar unos cambios en nuestro contenedor. Para ello, accedemos a él a través con shell de bash:

$ docker exec -it web /bin/bash

Y cambiamos el contenido del fichero index.html al que apunta el servicio de ngnix:

 $ sudo echo ‘<h1>Qualoom Expertise Technology</h1> > /usr/share/nginx/html/index.html

Para ver los cambios, refrescamos nuestro navegador:

docker10

No obstante, como apuntábamos al comienzo del artículo, esta no es la forma óptima de realizar este tipo de cambios, ya que si el contenedor muere, por el motivo que fuese, estos cambios quedarían revertidos, y veríamos de nuevo el contenido original tras llevar a cabo la instalación de Nginx que mostramos al principio.

En futuros artículos haremos un repaso de cómo Docker funciona de manera interna y de los comandos básicos más actualizados y cómo afrontar situaciones como la que acabamos de exponer.