Introducción a Docker: Nginx

“Encantado de conoceros, voy a ser vuestro nuevo compañero en el equipo de desarrollo, ¿quién me ayuda a montar el entorno en mi máquina?”

La anterior frase se repite muy a menudo en todos los rincones de (¿cualquier?) compañía a día de hoy, pero la respuesta puede ser más o menos dolorosa según se hayan hecho las cosas previamente.

 

¿Qué queremos solucionar?

A través de cuatro publicaciones, siendo esta la primera, vamos a hacer una introducción a Docker y cómo nos puede ayudar a preparar entornos de desarrollo fácil de transportar de una máquina a otra. Para este caso he elegido un entorno basado en PHP con Nginx, al que más adelante le añadiremos Redis.

Es cierto que si no hay un trabajo previo bien hecho, construir el entorno de trabajo para un determinado proyecto puede ser un auténtico dolor, por mucho gestor de paquetes que tengas siempre hay algo extra que se necesita, y que acaba llenando los ficheros ocultos del sistema. Los que usamos MacOS conocemos de sobra Brew, y cómo nos ayuda instalar librerías y ejecutables de manera sencilla, pero lo que nadie te comenta es cómo hacer una buena gestión de todo lo instalado. En mi caso Docker ha estado “robándole” entradas a Brew: versiones de php, ejecutables, etc.

La posibilidad de igualar el software de entornos de staging, pre y prod en local es algo que Docker hace de maravilla. De esta manera podemos apuntalar mucho mejor el desarrollo a sabiendas de que dónde va a ejecutar finalmente nuestro trabajo no difiere mucho de dónde se está construyendo.

Y finalmente, el caso de uso de Docker más claro es el de poder decirle a tu compañero: “Bájate esta imagen de docker y ejecútala, tendrás corriendo el servicio que necesitas para desarrollar, no necesitas instalar nada más…”.

 

¿Cómo funciona y qué ofrece Docker?

Primero veamos un esquema sencillo de cómo funcionan las máquinas virtuales:

 

Las máquinas virtuales hacen uso del llamado Hypervisor, que actúa de “puente” entre los recursos del sistema padre y los invitados, emulando los recursos que necesitan los invitados. En este esquema tenemos 3 sistemas operativos invitados corriendo en nuestro sistema, y para cada uno de ellos tendrías que configurar de manera individual la aplicación necesaria y sus librerías.

Ahora veamos un esquema básico que sitúa a Docker en nuestro sistema:

 

Lo que tenemos es un demonio de docker que ha sustituido a la capa del Hypervisor. ¿Cuál es la diferencia? Este demonio no “emula” recursos, da acceso directo a los propios recursos del sistema padre, con una especie de guardián (seccomp) de por medio para no abusar, lo cual evita instalar otros sistemas operativos que ocupan más espacio y generan más gestión de recursos.

A grandes rasgos, Docker funciona instalando un demonio en la máquina que expone una API, y a esa API podemos acceder con un CLI o con una UI como Docker for Mac:

 

Lo más importante de cómo funciona Docker no es quizás eso, sino la capacidad de compartir las “librerías/binarios” entre aplicaciones/servicios. What? Sí, un poco loco, pero primero veamos 3 términos fundamentales del ecosistema docker:

  • Dockefile: conjunto de instrucciones/plantilla/receta que serán usadas para construir la imagen requerida.
  • Imagen: aplicación/servicio construido listo para funcionar.
  • Contenedor: imagen en funcionamiento.

 

Algunas analogías que ayudan a entender los conceptos:

  • Programación: la imagen equivale a la clase y el contenedor a una instancia de esa clase.
  • Esquema SQL 1 – * : de una imagen se pueden lanzar infinitos contenedores.

 

Lo que viene a continuación es un Dockerfile muy básico, con 7 instrucciones que lanza un pequeño server en nodejs:

 

En lenguaje natural:

  1. Descárgate de DockerHub la imagen de node etiquetada como argon.
  2. Crea un directorio /usr/src/app.
  3. “Muévete” a ese directorio (se establece como path por defecto para lo que venga a continuación).
  4. Copia el package.json (que está en mismo el path del Dockerfile) a ese directorio.
  5. Instala librerías npm.
  6. Copia el contenido de la carpeta actual del host a la carpeta /usr/src/app de la imagen.
  7. Expon puerto 8080 para que otros contenedores de la misma red de Docker se puedan conectar a él.
  8. Arranca servidor.

 

Y ahora es cuando explicamos por qué hemos dicho antes que Docker comparte recursos entre aplicaciones/servicios. Veamos la siguiente imagen:

Si leemos de abajo a arriba coincide con las instrucciones del Dockerfile, y a cada instrucción le asigna un hash. Si una instrucción determinada se repite a en la construcción de otras imágenes, Docker reutiliza esa instrucción para no tener que ocupar espacio extra en disco, y de esta manera no tener que ejecutarse de nuevo la siguiente vez que se construye la imagen. La imagen se construye con capas, como si de una cebolla se tratase, pudiendo mover las capas a otra imagen que las necesite, como si de una caché de instrucciones se tratase.

Una posible pregunta en este punto sería: “¿Qué pasa si cambio mi package.json? ¿Me estás diciendo que esa instrucción de copiar el packgae.json se cachea, por lo que la carpeta node_modules generada del npm install estaría cacheada y no actualizada si yo hago un cambio en el package.json?

La respuesta es que eso es incorrecto. Cuando el hash de una “capa” cambia (por ejemplo: ha cambiado el contenido de los ficheros relacionados con esa capa), Docker invalida todas las instrucciones que vengan después. Fijaos en la posición que tienen el npm install, la copia del package.json y la copia de todos los ficheros del directorio actual. Cuando el package.json cambia, Docker invalida tanto el npm install como la copia de ficheros, por lo que se realizaría de nuevo un npm install, que es lo que queremos. Imaginad que la copia de ficheros la hacemos antes del npm install, en ese caso cada vez que cambiase un fichero se realizaría un npm install (que es lo que queremos evitar), de ahí que la colocación de instrucciones no sea arbitraria y lleve consigo una estrategia a la hora de construir imágenes.

 

Resumiendo

En estos momentos estamos en condiciones de enumerar los beneficios más importantes de Docker:

  • Espacio ocupado: una VM necesita un OS completo instalado en el host para poder ejecutar aplicaciones en él, mientras que Docker funciona sobre el host directamente, además de compartir recursos con el resto de entidades creadas para correr en el sistema.
  • Compatiblidad: el Dockerfile es lo único que necesitas para mover una aplicación de un sistema a otro, que con que tenga instalado Docker es suficiente, y a día de hoy Docker es compatible con la mayoría de sistemas del mercado.
  • Rapidez: arrancar una aplicación cuya imagen ya está construida es cuestión de segundos o incluso milisegundos. El trabajo grande se realiza al crear la imagen, pero lanzar un contenedor de esa imagen es muy rápido.
  • Experimentación: se acabó tener diferentes versiones de aplicaciones en el sistema host. Con el etiquetado de imágenes puedo estar lanzando dos contenedores a la vez, uno con PHP5.6 y otro con PHP7.3 sin tener que instalar nada en el SO host.

Hasta ahora os he contado por encima los detalles que a mi parecer son los más importantes a la hora de introducirse en el mundo de Docker, pero básicamente estamos rascando la superficie del ecosistema Docker.

 

Taller: ejercicio básico con Nginx

Asumiendo que tenéis Docker instalado en el sistema, ejecutad docker run hello-world y, si os salen instrucciones de qué ha sucedido al ejecutar el comando, querrá decir que está instalado correctamente, por lo que podremos proceder con el ejercicio.

A continuación vamos a crear nuestro primer Dockerfile para crear una imagen de Nginx con un HTML nuestro, y una vez hecho eso lanzaremos el contenedor para verlo funcionar. Tenéis el código en el repo de Github.

 

Propongo un Dockerfile como el siguiente:

FROM nginx:latest
COPY default.conf /etc/nginx/conf.d/default.conf
RUN mkdir -p /www/myapp
COPY . /www/myapp
LABEL maintainer="Andrew <XXXXXXYYYZZZZ@gmail.com>" \ version="1.0"
CMD ["nginx", "-g", "daemon off;"]

Traduciendo:

  1. Descárgate la última versión de la imagen de Nginx de DockerHub, será la que usemos de base.

  2. Copia mi fichero específico de configuración a la carpeta de configuración de Nginx.

  3. Crea la carpeta /www/myapp.

  4. Copia todo el directorio actual a esa carpeta.

  5. Añade unas etiquetas, visibles a posteriori en la información de la imagen.

  6. Comando con el que arrancará cualquier contenedor que se lance con esta imagen. Si el proceso mostrado termina, el contenedor también lo hace.

 

Ya tenemos nuestro fichero mágico, siguiente paso: construir una imagen. Para ello acudimos a nuestra maravillosa terminal, vamos al path donde se encuentra el Dockerfile y ejecutamos:

docker image build -t test-nginx .

Traduciendo:

Construye una imagen y llámala test-nginx, busca un Dockerfile en el path actual (por defecto ese nombre, aunque se puede indicar otro), y el contexto sobre el cual se va a trabajar será el path actual (.). El contexto es el root path desde donde se van a ejecutar instrucciones como COPY.

Si ejecutáis docker image ls veréis vuestra imagen en el listado.

 

Siguiente paso, arrancar un contenedor de esa imagen. Otra vez a la terminal:

docker container run --rm -it -p 8081:80 --name test-nginx test-nginx

Traduciendo:

Arranca un contenedor de la imagen test-nginx, llámalo también test-nginx para que cuando haga docker container ls aparezca con ese nombre. Además quiero que se enlace el puerto 8081 de mi localhost, al 80 del contenedor (que es donde se va ejecutar Nginx), y si el contenedor se para quiero que se borre automáticamente de los contenedores creados (para eso es --rm , sino se quedará parado esperando a ser iniciado de nuevo). Y como parámetro extra añadimos las flags -it para mantener el STDIN abierto y una TTY para conectarme al contenedor.

 

En estos momentos deberíais tener [localhost:8081](<http://localhost:8081>) habilitado con esta pequeña web:

“¡Ey Andrés! ¿y ese último comando que sale ahí?”

Ahora viene la auténtica gracia de todo esto. Si modificáis el html no veréis ningún cambio porque ese html se metió a la hora de construir la imagen. Pero podemos “mapear” carpetas de nuestro sistema de ficheros con el volumen del contenedor, de manera que cualquier cambio que sufra nuestra carpeta se vea reflejado en el contenedor. Para ello haremos uso de la opción -v $PWD:/www/myapp para sincronizar nuestra carpeta con la del contenedor. Ahora si arrancáis de nuevo el contenedor, cambiáis algo del HTML y recargáis el navegador, veréis el cambio. ¡Tachán!

 

Hasta aquí ha llegado esta introducción. La siguiente vez que nos veamos será para afianzar estos conceptos y para añadir PHP a la ecuación. ¡Hasta la próxima!

Leave a Reply