Original article: How to Enable Live-reload on Docker-based Applications with Docker Volumes

En este post aprenderás cómo configurar un entorno de desarrollo con recarga en vivo habilitada (live reload). Esto te permitirá convertir una aplicación legacy (código anticuado u obsoleto) para que use Docker, volúmenes de Docker y docker-compose.

Algunos desarrolladores levantan sus narices cuando hablan sobre usar Docker para su entorno de desarrollo. Dicen que Docker no es bueno para el desarrollo porque siempre necesita reconstruir toda la imagen para reflejar todas las nuevas modificaciones. Esto lo hace improductivo y lento.

En este artículo, nuestro objetivo es cambiar esta manera de pensar demostrando cómo las configuraciones simples pueden resultar en muchos beneficios como un entorno confiable sobre entornos de producción y desarrollo.  

Al final de este post habrás aprendido a cómo:

  • Convertir una aplicación "legacy" para que se ejecute dentro de un contenedor de Docker.
  • Permitir el cacheo de dependencias en módulos de Node.js.
  • Permitir la recarga en vivo usando volúmenes de Docker.
  • Agregar todos los servicios dentro de docker-compose.

Requerimientos

En los próximos pasos, clonarás un proyecto existente para ejecutar todos los ejemplos en este artículo. Antes de empezar a codificar asegúrate de tener las siguientes herramientas instaladas en tu máquina:

¿Por qué usar Docker?

Más y más tecnologías innovadoras están siendo lanzadas al internet todo el tiempo. Son estables, y son divertidas para desarrollar y lanzar, pero su comportamiento no es consistente al trabajar sobre diferentes entornos. Así que los desarrolladores crearon Docker para ayudar reducir las probabilidades de posibles errores.

Docker es una de mis herramientas favoritas con la que trabajo todos los días en el escritorio, en la web y con aplicaciones IoT. Me ha dado el poder de no solamente mover aplicaciones a través de diferentes entornos, sino también de mantener mi entorno local lo más limpio posible.

Los desarrolladores que trabajan con tecnologías innovadoras siempre están trabajando con algo nuevo. Pero ¿qué hay de las aplicaciones legacy? ¿Deberíamos sólo reescribir todo con nuevas tecnologías? Sé que esto no es tan simple como parece. Deberíamos trabajar en nuevas cosas, pero también hacer mejoras a las aplicaciones existentes.

Digamos que te has decidido a mover servidores de Windows a servidores de Unix. ¿Cómo lo harías? ¿Conoces la lista completa de cada dependencia que tu aplicación requiere para trabajar?

¿Cómo debería lucir un entorno de desarrollo?

Los desarrolladores siempre han intentado de ser más productivos agregando plugins, boilerplates, y bases de código en sus editores/IDEs/terminales. El mejor entorno en mi opinión debería de ser:

  1. Fácil de ejecutar y probar.
  2. Ser un entorno agnóstico.
  3. Rápido de evaluar modificaciones.
  4. Fácil de replicar en cualquier máquina.

Siguiendo estos principios, configuraremos una aplicación a lo largo de las siguientes secciones de este artículo. También, si nunca has oído sobre recarga en vivo (o recarga caliente), es una característica que observa los cambios en tu código y reinicia el servidor si es necesario. Así que no necesitas ir y venir, reiniciando tu aplicación o inclusive reconstruyendo el sistema.

Comenzando

Primero, necesitarás tener una carpeta vacía llamado post-docker-livereload el cual usarás como un espacio de trabajo. Ve al repositorio de Github y clónalo en tu carpeta post-docker-live-reload.

Segundo, analicemos lo que la aplicación requiere. Si miras al archivo README.md, hay algunas instrucciones indicando cómo ejecutar esta aplicación como se muestra en la imagen de abajo:

Screen-Shot-2020-06-24-at-18.10.43-1

Se requiere la versión 10 de Node.js o superior y MongoDB. En vez de instalar MongoDB en tu máquina de entorno local, lo instalarás usando Docker. También lo expondrás en localhost:27107 así las aplicaciones que no se ejecutan a través de Docker podrán acceder sin conocer la dirección IP interna de Docker.

Copia el comando de abajo y pégalo en tu terminal:

docker run --name mongodb -p 27017:27017 -d mongo:4

Usando el comando de arriba, descargará y ejecutará la instancia de MongoDB. Fíjate que si ya tienes una instancia con este nombre lanzará un error de nombre inválido.

Si ves el error, ejecuta docker rm mongodb y de esta forma removerá cualquier instancia previa, así podrás ejecutar el comando docker run nuevamente.

Profundizando en la aplicación

El archivo README.md dice que necesitas una instancia de MongoDB ejecutándose antes de comenzar tu aplicación, en conjunto con Node.js.

Si tienes Node.js instalado, ve a la carpeta nodejs-with-mongodb-api-example y ejecuta los siguientes comandos:

npm i 
npm run build 
npm start

Después de ejecutar estos comandos, puedes ir al navegador en http://localhost:3000 y ver la aplicación ejecutándose como se muestra en la imagen de abajo:

01-start

Ten en cuenta que la aplicación ya tiene un comando de activar recarga en vivo el cual es npm run dev:watch. El pipeline (flujo o embudo) debería reflejar los siguientes pasos:

  1. El desarrollador edita archivos de Typescript.
  2. Typescript "transpila" (compilar entre lenguajes) los archivos a JavaScript.
  3. El servidor ve los cambios realizados en JavaScript y reinicia el servidor de Node.js.

Así que duplicar los archivos a contenedores de Docker reflejará todos los cambios en el contenedor. El npm run build:watch de la aplicación capturará los cambios y generará archivos de salida en la carpeta lib, de esa forma npm run dev:run reiniciará el servidor cada vez que se haya disparado.

Dockerizando aplicaciones

Si Docker es un mundo completamente nuevo para ti, ¡no temas! Lo configurarás desde cero. Necesitarás crear un par de archivos para empezar:

  1. Dockerfile - un archivo de recibo, que enlista instrucciones para instalar y ejecutar tu aplicación.
  2. .dockerignore - archivo que enlista cuáles archivos no irán dentro de la instancia del contenedor de Docker.

Creando el archivo Docker

El archivo Docker es el concepto clave aquí. Allí especificas los pasos y dependencias para preparar y ejecutar la aplicación. Siempre y cuando hayas leído el archivo README.md, será fácil de implementar el archivo de recibo.

Voy a poner todo el archivo abajo y lo indagaremos más adelante. En tu carpeta nodejs-with-mongodb-api-example crea un archivo Dockerfile y pega el código de abajo:

FROM node:14-alpine

WORKDIR /src

ADD package.json /src 

RUN npm i --silent

ADD . /src 

RUN npm run build 

CMD npm start

¿Qué está ocurriendo ahí?

  • En la línea 1. Usa como su imagen base la versión alpine de Node: Node.js 14.
  • De las líneas 2 a 4. Copia e instala dependencias de Node.js del host al contenedor. Fíjate que el orden ahí es importante. Agregar un archivo package.json a la carpeta src antes de restaurar las dependencias los guardará en caché y prevendrá que reinstale los paquetes cada vez que necesites construir tu imagen.
  • De las líneas 6 a 7. Ejecuta comandos para el proceso de compilación y luego comienza el programa como se menciona en el archivo README.md.

Ignorando archivos innecesarios con .dockerignore

También, estoy trabajando en un sistema basado en OSX y el contenedor de Docker se ejecutará en un sistema basado en Linux Alpine. Cuando ejecutes npm install restaurará las dependencias para los entornos específicos.

Ahora crearás un archivo para ignorar el código generado desde tu máquina local como node_modules y lib. Así que cuando copies todos los archivos del directorio actual al contenedor este no tendrá versiones de paquetes inválidos.

En la carpeta nodejs-with-mongodb-api-example crea un archivo .dockerignore y copia el código de abajo:

node_modules/
lib/

Construyendo la imagen de docker

Prefiero ejecuta esta aplicación desde la carpeta rootFolder. Regresa a la carpeta post-docker-live-reload y ejecuta los siguientes comandos para preparar una imagen para su posterior uso:

docker build -t app nodejs-with-mongodb-api-example

Fíjate que el comando de arriba usa la bandera -t para especificar el nombre de la imagen y, justo después de eso, la carpeta que contiene el archivo Dockerfile.

Trabajando con volúmenes

Antes de ejecutar la aplicación, hagamos algunos trucos para mejorar nuestra experiencia en los contenedores de Docker.

Los volúmenes de Docker son una característica que permite duplicar archivos a entre tu máquina local y tu entorno de Docker. También puedes compartir volúmenes en los contenedores y reusarlos para cachear dependencias.

Tu objetivo es mirar cualquier cambio en los archivos locales .ts y duplicar esos cambios en el contenedor. Aunque los archivos y la carpeta node_modules están en la misma ruta.

¿Recuerdas que dije que las dependencias en cada sistema operativo serían diferentes? Pues para asegurarnos de que nuestro entorno local no afectará al entorno de Docker cuando se dupliquen archivos, aislaremos la carpeta node_modules del contenedor en un volumen distinto.

Consecuentemente, cuando se crea la carpeta node_modules en el contenedor, esto no creará la carpeta en el entorno de la máquina local. Ejecuta el comando de abajo en tu terminal para crearla:

docker volume create --name nodemodules

Ejecutar y activa recarga en vivo

Como sabes, el npm run dev:watch especificado en el README.md te muestra cómo activar la recarga en vivo. El problema es que estás codificando en una máquina local y debe reflejarse directamente en tu contenedor.

Ejecutando los siguientes comandos enlazarás tu entorno local con el contenedor de Docker así que cualquier cambio en nodejs-with-mongodb-api-example afectará la carpeta src del contenedor.

docker run \
    --name app \
    --link mongodb \
    -e MONGO_URL=mongodb \
    -e PORT=4000 \
    -p 4000:4000 \
    -v `pwd`/nodejs-with-mongodb-api-example:/src \
    -v nodemodules:/src/node_modules \
    app npm run dev:watch

Indaguemos que está ocurriendo ahí:

  • --link da permiso a la aplicación para acceder a la instancia de MongoDB.
  • -e son las variables de entorno. Como se mencionó en el archivo README.md puedes especificar la cadena de conexión de MongoDB que quieres conectar sobrescribiendo la variable MONGO_URL. Sobrescribe la variable PORT si quieres ejecutarlo en un puerto distinto. Fíjate que el valor mongodb es el mismo nombre que usamos para crear nuestra instancia de MongoDB en las secciones anteriores. Este valor es también un alias para la IP de la instancia interna de MongoDB.
  • -v - vincula el directorio actual al contenedor de Docker usando un volumen virtual. Usando el comando pwd puedes obtener la ruta absoluta hacia tu directorio de trabajo actual y luego la carpeta que quieres duplicar en el contenedor de Docker. Está el :/src. La ruta src es la instrucción WORKDIR definida en el Dockerfile por lo tanto duplicamos la carpeta local en la carpeta src del contenedor de Docker.
  • -v el segundo volumen ahí duplicará el volumen individual que creamos en la carpeta node_modules del contenedor.
  • app el nombre de la imagen.
  • npm run dev:watch este último comando sobrescribirá la instrucción CMD desde el archivo Dockerfile.

Después de ejecutar el comando de abajo, puedes lanzar el navegador después de cambiar el archivo index.ts para ver los resultados. El video de abajo demuestra estos pasos en la práctica:

Resumiendo

Sabes que trabajar con los comandos del shell funciona. Pero no es tan común para usarlos en este caso, y no es productivo ejecutar todos esos comandos; construyendo imágenes y gestionando instancias manualmente. ¡Así que usa compose!

Docker compose es una forma de simplificar la agregación y enlazamientos de servicios. Puedes especificar las bases de datos, logs, aplicación, volúmenes, redes, y así sucesivamente.

Primero, necesitas quitar todas las instancias activas para evitar conflictos en puertos expuestos. Ejecuta los siguientes comandos en tu terminal para remover volúmenes, servicios y contenedores:

docker rm app 
docker volume rm nodemodules
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)

El archivo docker-compose

Crea un archivo docker-compose.yml en tu carpeta post-docker-livereload usando los datos de abajo:

version: '3'
services:
    mongodb:
        image: mongo:4
        ports:
            - 27017:27017
    app:
        build: nodejs-with-mongodb-api-example
        command: npm run dev:watch
        ports:
            - 4000:4000
        environment: 
            MONGO_URL: mongodb
            PORT: 4000
        volumes:
            - ./nodejs-with-mongodb-api-example:/src/
            - nodemodules:/src/node_modules
        links:
            - mongodb
        depends_on: 
            - mongodb

volumes:
    nodemodules: {}

El archivo de arriba especifica los recursos por secciones. Fíjate que tienes las secciones links y depends_on allí. El campo links es la misma bandera que has usado en tu comando de shell. El depends_on se asegurará que el MongoDB es una dependencia para ejecutar tu aplicación. Ejecutará el MongoDB antes de tu aplicación, ¡como si fuera magia!

Volviendo a tu terminal, para iniciar todos los servicios y construir la imagen de Docker, crear volúmenes y enlazar servicios, ejecuta el siguiente comando:

docker-compose up --build

Si necesitas remover todos los servicios creados antes por el Dockerfile también puedes ejecutar docker-compose down.

¡Docker es tu amigo!

Así es, mi amigo. Docker te puede ayudar para prevenir muchos posibles errores antes de que ocurran. Puedes usarlos para aplicaciones front y back end. Inclusive para IoT cuando necesites controlar el hardware, puedes especificar políticas para usarlo.

Para tus próximos pasos, recomiendo mucho que mires a orquestadores de contenedores tales como Kubernetes y Docker swarm. Podrían mejorar mucho más tus aplicaciones existentes y ayudarte a ir al siguiente nivel.

Gracias por leer

Aprecio mucho el tiempo que pasamos juntos. Espero que este contenido sea más que solo texto. Espero que te ayude a pensar mejor y programar mejor. Sígueme en Twitter y mira my blog personal donde comparto todo mi contenido de valor.

¡Nos vemos!