04 septiembre 2021

Cómo usar contenedores de Docker

 
La virtualización se basa en el engaño.

Con la virtualización pesada (es decir, la de VMware, VirtualBox o Xen) a un sistema operativo "invitado" se le engaña para que crea que está funcionando en un hardware dedicado, cuando en realidad este es compartido.

Con virtualización ligera (es decir, la de Docker) a una aplicación se la engaña para que crea que está utilizando un kernel propio de sistema operativo, cuando en realidad este está compartido. La clave es que en Linux todo lo que no sea el kernel se considera una aplicación por lo que Docker puede hacer que varias destribuiones de linux compartan el mismo kernel (el del sistema anfitrión). Dado que el nivel de abstracción del engaño es mucho mayor que el practicado en la virtualización pesada el consumo de recursos es mucho menor, de ahí el nombre de virtualización ligera. Es tan ligera que muchas aplicaciones se distribuyen en paquetes de docker (denominados contenedores) en loas que la aplicación se empaqueta junto al sistema operativo y las dependencias que necesita para ser ejecutado de una vez en otro sistema, sin enredar con las dependencias de este último.

Es cierto que hay cosas que no se pueden hacer con virtualización ligera, pero no son muchas. Para desarrollar y ejecutar una aplicación estándar "de nivel 7" no encontraremos ninguna limitación usando virtualización de Docker.


Instalación

Antiguamente se podía instalar docker desde el repositorio de paquetes de tu distribución. Actualmente, aunque puedan encontrarse paquetes de docker en los repositorios estándar, lo normal es que ya no funcionen. Para hacer funcionar Docker lo suyo es ignorar los paquetes del repositorio estándar de tu distribución y tirar de los repositorios oficiales de Docker. 

Docker facilita repositorios de paquetes para muchas distribuciones. Por ejemplo, aquí se pueden leer las instrucciones para instalar docker en un equipo Ubuntu. Lo que sí hay que tener en cuenta es que en caso de que estemos usando algún derivado de Ubuntu, como Linux Mint, vamos a necesitar personalizar esas instrucciones para configurar la versión en las listas de fuentes de apt en nuestro sistema.

Una vez instalado docker en nuestro ordenador, podemos ejecutar la aplicación de Hello World, incluido en un contenedor de ejemplo, para comprobar que todo funcione bien:

dante@Camelot:~/$ sudo docker run hello-world
[sudo] password for dante:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:7d91b69e04a9029b99f3585aaaccae2baa80bcf318f4a5d2165a9898cd2dc0a1
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/

For more examples and ideas, visit:
https://docs.docker.com/get-started/

dante@Camelot:~$

Ten en cuenta que aunque puedas ejecutar contenedores de docker puede ser que no puedas descargar nuevos e instalarlos sin ser usar sudo. Si no te resulta cómodo trabajar así, siempre puedes añadirte a tí mismos al grupo de docker:

dante@Camelot:~/$ sudo usermod -aG docker $USER
[sudo] password for dante:

dante@Camelot:~$

Puede ser que tengas que cerrar la sesión y volver a hacer login para que los cambios se activen. De esta manera podrás ejecutar comandos de docker como un usuario normal sin privilegios de administrador.


Uso

Podemos crear nuestros propios contenedores, pero eso es un tema para otro artículo. En este artículo vamos a usar contenedores personalizados por otros.

Para encontrar contenedores que sirvan de punto de partida, dirígete a  Docker Hub y teclea cualquier aplicación o distribución de Linux en su campo de búsqueda. La búsqueda devolverá muchas opciones. Si buscas una distribución monda y lironda lo mejor es usar una con la etiqueta "Official image". 

Supongamos que quieres probar una aplicación en Ubuntu Xenial, entonces elige Ubuntu en el listado de salida de la búsqueda y échale un ojo a la sección de "Supported tags...". Ahí se puede ver como se llaman las distintas versiones para ser descargadas. En nuestro caso tomaremos nota de las etiquetas "xenial" y "16.04".

Ahora que ya sabemos qué bajar, vamos a hacerlo con docker pull:

dante@Camelot:~/$ docker pull ubuntu:xenial
xenial: Pulling from library/ubuntu
528184910841: Pull complete
8a9df81d603d: Pull complete
636d9303bf66: Pull complete
672b5bdcef61: Pull complete
Digest: sha256:6a3ac136b6ca623d6a6fa20a7622f098b2fae1ac05f0114386ef439d8ca89a4a
Status: Downloaded newer image for ubuntu:xenial
docker.io/library/ubuntu:xenial


dante@Camelot:~$

Lo que hemos hecho ha sido bajar lo que se denomina una imagen. Una imagen es un paquete base con una distribución de linux específica con una serie de aplicaciones instaladas. Desde ese paquete base se pueden derivar tus propios paquetes personalizados o ejecutar instancias de esos paquetes base, esas instancias es lo que denominamos contenedores.

Si quieres comprobar cuantas imágenes tenemos disponibles sólo hay que usar docker images:

dante@Camelot:~/$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu xenial 38b3fa4640d4 4 weeks ago 135MB
hello-world latest d1165f221234 5 months ago 13.3kB



dante@Camelot:~$

Puedes iniciar instancias (es decir contenedores) desde esas imñagenes usando  docker run:

dante@Camelot:~/$ docker run --name ubuntu_container_1 ubuntu:xenial
dante@Camelot:~$

Usando el parámetro --name puedes asignar un nombre específico a un contenedor para identificarlo entre otros contenedores iniciados desde la misma imagen.

El problema de iniciar contenedores de esta manera es que se cierran inmediatamente. Si miramos el estatus de nuestros contenedores usando docker ps:

dante@Camelot:~/$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c7baad2f7e56 ubuntu:xenial "/bin/bash" 12 seconds ago Exited (0) 11 seconds ago ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 9 hours ago Exited (0) 9 hours ago focused_zhukovsky

dante@Camelot:~$

He usado el parámetro -a para mostrar todos los contenedores, no sólo los activos. De esa manera se puede ver que el contenedor ubuntu_container_1 finalizó su actividad casi inmediatamene tras suzomienzo. Eso ocurre porque los contenedores de docker están diseñados para ejecutar una applicación específica y cerrarse en cuanto esa applicación acaba. Como nosotros no dijimos qué aplicación ejecutar, el contenedor se limitó a cerrarse en cuanto se inició.

Antes de intentar nada más, vamos a borrar el contenedor anterior usandodocker rm. De esa manera empezaremos desde cero:

dante@Camelot:~/$ docker rm ubuntu_container_1
ubuntu_container_1

dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$

Ahora lo que queremos es mantener el contenedor vivo para poder acceder a su consola. Una manera de hacerlo es:

dante@Camelot:~/$ docker run -d -ti --name ubuntu_container_1 ubuntu:xenial
a14e6bcac57dd04cc777a4eac787a8465acd5b8d379591976c56de1d0acc2798

dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a14e6bcac57d ubuntu:xenial "/bin/bash" 8 seconds ago Up 7 seconds ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$

En el comando anterior hemos usado el parámetro -d para ejecutar el contenedor en segundo plano y los parámetros -ti para iniciar una consola interactiva y mantenerla abierta. Haciéndolo de esa manera, podemos ver que esta vez el contenedor se mantiene arriba. Pero seguimos sin acceder a la consola del contenedor, para acceder a ella debemos hacer docker attach:

dante@Camelot:~/$ dante@Camelot:~$ docker attach ubuntu_container_1
root@a14e6bcac57d:/#

Se puede ver que la consola ha cambiado su identificador del lado izquierdo. Se puede salir del contenedor tecleando exit, pero eso para el contenedor. Para dejar el contenedor manteniéndolo activo debemos usar Ctrl+p seguido de Ctrl+q en su lugar. 

Se puede para un contenedor inactivo para ahorrar recursos y reanudarlo después usando docker stop y docker start:

dante@Camelot:~/$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9727742a40bf ubuntu:xenial "/bin/bash" 4 minutes ago Up 4 minutes ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$ docker stop ubuntu_container_1
ubuntu_container_1
dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9727742a40bf ubuntu:xenial "/bin/bash" 7 minutes ago Exited (127) 6 seconds ago ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$ docker start ubuntu_container_1
ubuntu_container_1
dante@Camelot:~$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9727742a40bf ubuntu:xenial "/bin/bash" 8 minutes ago Up 4 seconds ubuntu_container_1
3ba89f1f37c6 hello-world "/hello" 10 hours ago Exited (0) 10 hours ago focused_zhukovsky
dante@Camelot:~$


Salvar los cambios

Será habitual que quieras exportar los cambios realizados a un contenedor para poder iniciar uno nuevo con esos cambios aplicados.

La mejor manera es usar dockerfiles, pero eso lo dejaré para un artículo posterior. Una manera rápida (y chapucera) es registrar los cambios en una nueva imagen haciendo docker commit:

dante@Camelot:~/$ docker commit ubuntu_container_1 custom_ubuntu
sha256:e2005a0ec8302f8948958a90e2abc1e8957a15155c6e6bbf7300eb1709d4ae70
dante@Camelot:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
custom_ubuntu latest e2005a0ec830 5 seconds ago 331MB
ubuntu xenial 38b3fa4640d4 4 weeks ago 135MB
ubuntu latest 1318b700e415 4 weeks ago 72.8MB
hello-world latest d1165f221234 5 months ago 13.3kB

dante@Camelot:~$

En este ejemplo hemos creado una nueva imagen llamada custom_ubuntu. Usando esa nueva imagen podemos crear nuevas instancias con los cambios hechos hasta el momento a ubuntu_container_1.

Ten en cuenta que al registrar los cambios de un contenedor en funcionamiento este se para con el fin de evitar la corrrupción de datos. Al finalizar el registro el contenedor reanuda su funcionamiento.


Ejecutar servicios desde contenedores

Hasta ahora hemos levantado una máquina virtual ligera y hemos accedido a su consola, pero eso no es suficiente si queremos ofrecer servicios desde ese contenedor.

Supongamos que hemos configurado un servidor de SSH en el contenedos y que queremos accedes a él a través de la LAN. En ese caso necesitaremos que determinados puertos del contenedor sean mapeados con otros del anfitrión que puedan ser accedidos por la LAN.

Una trampa importante a tener en cuenta aquí es que ese mapeo de puertos debe ser configurado cuando el contenedor se ejecuta por primera vez con docker run, usando el parámetro -p:
dante@Camelot:~/$ docker run -d -ti -p 8888:22 --name custom_ubuntu_container custom_ubuntu
5ea2604da8c696af397cd1b1f98d7426dd68182e5edd81a56a6b739849ba1e84

dante@Camelot:~$

Hecho eso se podrá acceder al servicio SSH del contenedor usando el puerto 8888 del anfitrión.


Compaartir ficheros con los contenedores 

Nuestros contenedores no serán islas aisladas. En ocasiones necesitarán que le facilitemos ficheros externos o nos devolverán ficheros.

Una manera de hacerlo es arrancar los contenedores montando una carpeta del anfitrión a modo de carpeta compartida:

dante@Camelot:~/$ docker run -d -ti -v $(pwd)/docker_share:/root/shared ubuntu:focal
ee64e7392a77b4d0b35adadad467fe5d6a105b84290945e346f82199116b7c56

dante@Camelot:~$

Aquí, la carpeta del anfitrión docker_share será accesible en la ruta del contenedor /root/shared. Ojo que las rutas deben ser absolutas. Utilizao $(pwd) como un atajo para poner el directorio actual.

Una vez que el contenedor esté arrancado, cualquier fichero que el contenedor sitúe en la carpeta /root/shared será visible desde el anfitrión, incluso después de que se pare al contenedor. En el otro sentido, es decir dejar un fichero del anfitrión para que lo vea el contenedor, es posible pero habrá que hacer sudo:

dante@Camelot:~/$ cp docker.png docker_share/.
cp: cannot create regular file 'docker_share/./docker.png': Permission denied

dante@Camelot:~/$ sudo cp docker.png docker_share/.
[sudo] password for dante:

dante@Camelot:~$

Otra manera es compartir los ficheros usando el comando de copia que tiene docker:

dante@Camelot:~/$ docker cp docker.png vigorous_kowalevski:/root/shared 
dante@Camelot:~$

Aquí, hemos copiado el fichero docker.png del anfitrión a la carpeta /root/shared del contenedor vigorous_kowalevski. Fíjate que no hemos necesitado hacer sudo para ejecutar el comando docker cp

En el otro sentido también es posible, sólo hay que cambiar el orden de los argumentos:

dante@Camelot:~/$ docker cp vigorous_kowalevski:/etc/apt/sources.list sources.list 
dante@Camelot:~$

Aquí, hemos copiado el fichero sources.list del contenedor al anfitrión.


A partir de aquí

Hasta ahora hemos visto como manejar contenedores de docker. El siguiente paso es crear tus propias imágenes usando dockerfiles y compartiendolos a través de Docker Hub. Explicaré todo eso en un artículo posterior.