De Docker Noob a Pro

Hola Fronters, Gabriel aquí!!! Haciendo un recuento de todo lo que hemos aprendido en el artículo anterior, vimos como revisar procesos de Docker como por ejemplo: el estado de los contenedores, con docker ps, utilizar comandos como docker run | start | stop | kill y como combinar estos otros comandos con la salida de docker ps para ejecutar acciones precisas a un determinado grupo de contenedores.

Cuando creamos un contenedor, lo más sorprendente es el poder levantar en tiempos sumamente cortos cualquier sistema operativo o aplicación. Sin embargo, aun no podemos podemos «enlazar» de manera óptima a nuestro contenedor con el servidor que contiene el motor de Docker (Docker Engine Server | service o también nos referiremos a este como DES). Recuerda que el Docker Server Engine, puede ser un ordenador / computador sencillo o portátil apto para correr dicho motor e implementar containers, y no necesariamente tiene que ser un mega servidor con recursos desorbitantes alojado en un centro de datos.

Si bien es cierto que, podemos crear contenedores en modo desatendido (detached), y luego retomarlos con docker attach, y así sucesivamente, no es suficiente a la hora de implementar servicios más complejos, como por ejemplo: un servicio web o un motor de base de datos. Por lo tanto necesitamos encontrar la manera de establecer una forma de vincular el servicio que ejecuto en nuestro contenedor, con el DES, es decir: con el exterior. Para ello, hemos de utilizar uno de los recursos más comunes de comunicación y enlace: el mapeo de puertos (Port Mapping).

Otro de los problemas que tenemos es que los contenedores no son persistentes por sí mismos, es decir: al culminar su tiempo de vida, los datos contenidos en este, se perderían por completo, lo que puede llegar a ser un verdadero quebradero de cabeza. Para ello haremos uso de volúmenes, los cuales vincularemos nuevamente desde nuestro DES hacia el container.

Mapeando puertos como expertos

Tal y como explicamos anteriormente, el mapeo de puertos es una técnica que permite comunicar nuestro contenedor con el exterior mediante la exposición de un puerto en específico, el cual se hará enlazándolo con un puerto local de nuestro DES. Para ellos debemos tener claro que el puerto a vincular estará íntimamente relacionado con el servicio que deseamos implementar en nuestro contenedor y no otro. En pocas palabras, si deseamos desplegar un servidor web, no tendría sentido exponer el puerto de SSH.

Bien, vamos a desenredar los dedos ya y vamos a crear un nuevo contenedor desde cero. El contenedor será un NGINX, el cual es un servidor Web, ligero y super eficiente. Para ello, lo primero que haremos es descargar la imagen oficial de NGINX:

docker search nginx

docker pull nginx
Buscando y descargando una imagen de Nginx para nuestro nuevo contenedor
Imagen 1: Buscando y descargando una imagen de NGINX para nuestro nuevo contenedor

Una vez que ya tenemos la imagen que vamos a utilizar, pues la implementaremos con esta sintaxis:

docker run --name nombrecontainer -d -p puertodes:puertocontainer imagen:version

Entonces, en el caso del servicio NGINX que deseamos implementar, crearemos el contenedor que llamaremos web, en modo desatendido, mapeando el puerto 80 del contenedor hacia el puerto 8080 de nuestro servidor DES:

docker run --name web -d -p 8080:80 nginx:latest

Para verificar si creamos el contenedor y su estado de funcionamiento ejecutamos docker ps -l:

docker ps -l
Creando nuestro contenedor web y verificando su estado
Imagen 2: Creando nuestro contenedor web y verificando su estado

Y vemos que efectivamente nuestro contenedor se está ejecutando y que el puerto 80 de nuestro contenedor efectivamente recibe peticiones del exterior al estar mapeado al puerto 8080 de nuestro servidor. Para constatar por nuestros propios ojos de que esto es así, simplemente ejecutamos el siguiente comando desde nuestro servidor DES:

curl localhost:8080

Y veremos el código HTML de la página inicial de NGINX de nuestro contenedor:

Resultado del mapeo de puertos entre nuestro contenedor NGINX y el Docker Engine Server.
Imagen 3: Resultado del mapeo de puertos

IMPORTANTE: Para efectos de elaboración de esta serie de artículos y posteriores, no he instalado Docker directamente en mi ordenador, sino que lo ejecuto desde otra instancia, en un servidor aparte, por lo tanto no utilizaré muy a menudo el término localhost en las imágenes cuando vaya a ejecutar acciones desde el navegador web, sino que utilizaré la dirección IP de ese servidor. Te especificaré cuando puede que sea necesario para que vayamos al mismo ritmo.

Cuando queramos hacer esto mismo desde un ordenador / computador, simplemente abriremos el navegador web de nuestra preferencia y escribiremos:

Si es desde el mismo ordenador en donde tenemos instalado el Docker Engine Service:

http://localhost:8080

Si es en un ordenador diferente (siempre y cuando estén dentro de la misma red LAN):

http://ipdockerengineserv:8080

Es decir: suponiendo que tenemos un contenedor web, con NGINX corriendo en un servidor DES cuya dirección IP es la 192.168.16.60:

http://192.168.16.60:8080

Viendo entonces la página inicial de NGINX corriendo desde el navegador web:

Viendo la página inicial de NGINX de nuestro contenedor mapeado desde un web browser
Imagen 4: Viendo la página inicial de NGINX de nuestro contenedor mapeado desde un web browser

Una pausa en este punto no nos vendría mal. Nos vemos después del café…

La ausente persistencia y como solventar tal incidencia

La persistencia en el contenedor es algo muy importante en lo que debemos concentrarnos antes de generar cualquier otro cambio en nuestros contenedores, pues, retomando un poco lo que decíamos en párrafos anteriores, es lo que garantizará la persistencia de los datos en el tiempo y ante cualquier incidencia. Lo explicaremos en un caso hipotético, pero que para más de lo que se imaginan.

Imaginemos que estamos montando una intranet, algo sencillo, quizás estático… Quizás con un CMS (WordPress, Joomla, etc). Hacemos los cambios DENTRO del contenedor, trabajamos por etapas, todo va genial, diseño apariencia, las secciones… Todo hermoso, está casi lista. Y de repente… Por alguna razón inesperada, falla el suministro de electricidad. Al volver a levantar, ¿qué tenemos en nuestro contenedor?, Nada…

La forma más básica de darle persistencia a un contenedor es mediante la utilización de volúmenes.

Qué son los volúmenes y cómo utilizarlos

Vamos a introducir un concepto nuevo. Un volumen es simplemente un directorio que vinculamos desde nuestro servidor DES hacia nuestro contenedor. Ese espacio va a permitir la persistencia de archivos / ficheros en el tiempo, sin necesidad de hacer algún tipo de cambio en la estructura del contenedor y sin importar si el contenedor se detiene, bien sea de forma abrupta, controlada (o incluso ambas), y luego se continúa su ejecución.

Para montar un volumen es relativamente simple, pero hay una serie de pasos a seguir. Lo haremos en la mejor manera posible para que se pueda entender.

Procedimiento :

Creamos un directorio en la ubicación predeterminada de nuestro sistema para dicha tarea, en este caso seleccionaremos el directorio /home:

mkdir /home/$USER/web

Crearemos un archivo en esa ruta que se llame index.html y le pondremos un contenido básico en HTML.

nano /home/$USER/web/index.html

Para efectos prácticos podemos utilizar el siguiente código HTML :

<html>

<head>
  <title>IT Frontech.com - Index</title>
</head>

<body>
  <h1>Esta es mi primera Web de prueba</h1>
  <p>Esta es mi primera web desde mi contenedor persistente. Chao</p>
</body>

</html>

Luego de esto, vamos a construir nuestro container con la siguiente sintaxis:

docker run --name nombrecont -v /origen/ruta/vol:/dest/ruta/montaje:perm -d -p portext:portcont imagen

Que en resumidas cuentas, sería así:

docker run --name web -v /home/$USER/web:/usr/share/nginx/html:rw -d -p 8080:80 nginx:latest

¿Que fue lo que hicimos?, Simple:

Creamos el contenedor web en modo desatendido (con el atributo -d), al cual le compartiremos como volumen ( con el atributo -v), la ruta /home/$USER/web y que será el contenido que se mostrará en el directorio /usr/share/nginx/html con permisos de lectura y escritura (rw); a su vez, mapeamos (con el atributo -p) el puerto 8080 del DES para que se conecte con el puerto 80 del contenedor y por último pero no menos importante, le diremos a Docker que utilizaremos la imagen más reciente de NGINX.

Contenedor web corriendo con mapeo de puerto 80 al 8080 y volumen
Imagen 5: Contenedor web corriendo con mapeo de puerto 80 al 8080 y volumen

Entonces, cuando creemos nuestro contenedor y le indiquemos la ruta de nuestro volumen y los permisos, veremos el fichero / archivo HTML que creamos al entrar a la ip de nuestro DES, en mi caso sería http://192.168.16.60:8080/ (en el de ustedes podría ser localhost o la ip en la cual instalaron y configuraron su servicio de Docker):

Ejecutando nuestro fichero / archivo HTML creado en el volumen desde el contenedor web
Imagen 6: Ejecutando nuestro fichero / archivo HTML creado en el volumen desde el contenedor web

Casos de la vida real

Rayos, se fue la electricidad!!!, esto es más común de lo que imaginamos y no siempre estamos 100% preparados para esto. Será que esto afectará al contenido de mi contenedor? Pues al iniciar nuestro contenedor, nuevamente tendremos nuestros todos nuestros ficheros / archivos y directorios en el mismo sitio hasta el último guardado realizado.

docker start web

El resultado será este: Psst… Spoiler: No hemos perdido nada =) :

Levantando contenedor web luego de percance demostrando persistencia
Imagen 7: Levantando contenedor web luego de percance demostrando persistencia

Permisos de los volúmenes

Perfecto!!! No hemos perdido nada, sin embargo, asignamos permisos de lectura y escritura al volumen que montamos en el contenedor web, pero realmente no necesitamos que nadie más pueda escribir en él, así que cambiaremos los permisos a sólo lectura, con el atributo ro.

No obstante, si ejecutamos el comando anterior sin haber eliminado antes el container web nos dará error, en este caso, lo que haremos es detenerlo, eliminarlo y volver a crearlo con el nuevo atributo:

docker stop web

docker rm web

docker run --name web -v /home/$USER/web:/usr/share/nginx/html:ro -d -p 8080:80 nginx:latest

Podemos utilizar docker kill <nombrecontenedor> en vez de docker stop para terminar abruptamente la ejecución de un contenedor, pero docker kill se utiliza más que nada para contenedores que se tornen pesados en su ejecución, o que ya cumplieron su función y no son requeridos.

Consejo, del Tío Gabo: NO lo hagan en servidores de Bases de Datos en ejecución cuyos datos requieran posteriormente: hagan los backups necesarios, detengan los servicios y luego eliminen el contenedor.

Al volver a ejecutar el contenedor, y entrar desde éste y tratar de generar un cambio en su estructura, por ejemplo, crear un nuevo archivo / fichero, veremos que no podremos crear nada dentro de él porque nuestros permisos fueron cambiados.

Recreando contenedor web con permisos de solo lectura en el volumen
Imagen 8: Recreando contenedor web con permisos de solo lectura en el volumen

Cierre y Despedida

Ya habiendo visto todo lo anterior, podremos ser capaces de crear y modificar contenedores, adjuntando volúmenes y mapeando los puertos hacia el exterior, en el DES (Recuerda: Docker Engine Service / Server).

Les recuerdo además que si tienes dudas o preguntas puedes colocarlas en los comentarios y así aprendemos todos un poco más y hacemos crecer a esta bonita comunidad.

Ya no más por hoy, hasta luego Fronters!!!

Referencias

Docker: Documentación Oficial