Ejecutar aplicaciones multi-contenedor con docker-compose
Tengo que decir que cuanto más me adentro en el mundo Docker más me está gustando la experiencia. Sin embargo, he de reconocer que a veces es difícil elegir por dónde continuar contándote. Seguro que cada uno eligiríamos un camino distinto, ya que son tantos conceptos y tantos puntos a tener en cuenta que a veces no es fácil priorizar. Lo que si que creo es que, cuando empiezas con cualquier tecnología, es importante de que te surja la necesidad de algo más, ya que así entiendes el por qué de otras características. Por ejemplo, todavía no hemos visto nada de volúmenes, clústers, redes, etcétera pero creo que es importante ir poco a poco, al menos para que vayas entendiendo el por qué de las cosas.
Para terminar la semana, hoy te quiero contar un tema super interesante y es la ejecución de aplicaciones multi-contenedor. Si has seguido los últimos artículos, empecé con una aplicación demasiado sencilla llamada nodejs-webapp, que básicamente era un servidor web que devolvía código HTML estático sin más.
Era más que suficiente para ver cómo es posible ejecutar una aplicación con Node.js en un contenedor, cómo usar Docker Hub o Azure Container Registry, o incluso cómo ejecutar imágenes en Azure App Service, pero lo cierto es que las aplicaciones no son así. Las aplicaciones suelen estar compuestas de diferentes servicios, como una base de datos, APIs, un servidor web, etcétera.
¿Y cómo encajamos todas estas piezas en contenedores? Lo primero que podríamos pensar es en meter todo dentro de un contenedor, lo cual queda totalmente descartado.
En primer lugar, porque desde hace mucho mucho tiempo tenemos que pensar en aplicaciones distribuidas, en componentes independientes que trabajen en conjunto pero que puedan ser mantenidos, actualizados y escalados por separado. Obviamente, cada aplicación es un mundo pero lo que está claro es que cada componente dentro de mi aplicación debería de tener un número limitado de responsabilidades bien definidas. En este artículo voy a abandonar nuestro nodejs-webbapp y voy a usar algo un poco más real para crear una aplicación multi-contenedor.
Aplicación de ejemplo
Mi aplicación va a constar de tres partes o servicios:
- front end: será un servidor web en Node.js que servirá la web que ve el usuario final.
- back end: servidor web que contiene una API que me permite manipular un conjunto de topics.
- mongodb: una base de datos, de tipo mongodb, donde almacenaré los topics.
Como ves, ya no se trata sólo de un sitio web sino que tenemos tres componentes bien diferenciados, cada uno con sus propias responsabilidades.
Front end
No quiero complicarlo mucho, pero sí que quiero usar React.js para mostrar el contenido en la web. He creado una carpeta, multi-container-app, donde voy a añadir el contenido de dos de las tres imágenes que voy a crear, de frontend y backend. Dentro de este directorio ejecuta el siguiente comando para crear frontend.
Lanzando este comando se generará el esqueleto completo de las aplicaciones que utilizan este framework, por lo que es muy sencillo empezar. Instala también el módulo semantic-ui-react, que te ayudará a que quede algo más bonito.
Una vez que estén todos los paquetes instalados, abre el archivo App.js y reemplázalo con lo siguiente:
Si no conoces React.js es una tarea más que tienes pendiente. En este componente lo único que hago es que cuando se carga (componentWillMount) utilizo fetch para hacer una llamada al servicio de backend, que todavía no hemos creado, para recuperar los topics.
Para que los estilos de semantic-ui se vean correctamente, añade dentro de la etiqueta head del archivo public/index.html el siguiente enlace a semantic.min.css.
Al igual que en nuestra aplicación nodejs-webapp necesitamos crear dos archivos más: Dockerfile, donde incluiremos la receta necesaria para generar la imagen de este servicio:
Y el archivo .dockerignore con aquellos archivos y directorios que no queremos que docker build tome en cuenta.
Como ya sabes, para generar una imagen de frontend ejecuta el siguiente comando dentro de su directorio:
En el primer artículo de esta serie te enseñe a utilizar el etiquetado para que pudieras poner la versión que quisieras a tus imágenes. Además utilizábamos –tag=frontend. En este quería enseñarte otra forma de hacer lo mismo. Sin embargo, al no proporcionar una etiqueta lo que ocurrirá es que utilizará la llamada latest que indica que es la última versión.
Back end
Ahora llega el momento del back end. Como ya has visto en el apartado anterior, lo único que voy a hacer es generar un par de métodos para listar y añadir topics. Para ello voy a usar Express.js y como sistema de almacenamiento utilizaré MongoDB, que será el último servicio de esta aplicación. Crea una nueva carpeta dentro de multi-container-app llamada backend y utiliza el siguiente comando para generar el archivo package.json.
Ahora crea un archivo llamado server.js y copia el siguiente código:
Para que esta API funcione necesitas instalar los módulos express, cors, mongoose y body-parser. Puedes hacerlo a través del siguiente comando:
Además, como es la API de topics la que hace uso de un MongoDB, crea un archivo llamado /models/topics.js donde definirás el modelo de un topic.
Al igual que el front end, el servicio de back end también necesita un archivo Dockerfile, e idealmente un .dockerignore. Este último podemos copiarlo del que hemos usado en el front end. En cuanto al archivo Dockerfile, cambia ligeramente:
En este caso estamos exponiendo el puerto 8080. Ejecuta el siguiente comando, dentro del directorio backend para comprobar que la imagen se crea correctamente.
Ya tenemos nuestros dos servicios, uno que servirá la página web, a la que accederá el usuario, y un back end que facilitará el acceso a un MongoDB a través de una API. Para finalizar necesitamos una nueva receta que indique cómo deben trabajar juntos dentro de una misma aplicación, o como lo llaman en la documentación de Docker, stack.
Utilizando docker-compose
Si con docker run ejecutábamos un contenedor, con docker-compose seremos capaces de ejecutar varios a la vez. Para poder ejecutar este comando, antes de nada necesitamos un archivo más, de esos que yo llamo receta. Puedes crearlo donde quieras, pero yo prefiero ponerlo en la raíz de multi-container-app.
He intentado dejarlo lo más simple posible, pero sé que hay muchas cosas que se pueden o deben añadir a este archivo, pero esto es lo mínimo que necesitas. Como ves, en él aparecen los tres servicios de mi aplicación: frontend, backend y mongodb. En los tres se indica cuál es la imagen que se utilizará para crear el contenedor. Además, tanto en frontend como en backend aparece el mapeo de puertos con el mundo exterior, para que podamos acceder desde fuera. En el contenedor donde se ejecutará mongodb no es necesario, a no ser que también quieras acceder. La propiedad depends_on me ayudará a iniciar los servicios en orden, ya que el frontend no puede funcionar correctamente si el backend no se ha iniciado y el backend no funcionará si mongodb no está todavía listo.
Con esta configuración mínima, ya estás listo para ejecutar tu aplicación multi-contenedor. Para arrancar los tres componentes a la vez, ejecuta el siguiente comando, en la misma ubicación donde tienes el archivo docker-compose.yml:
En unos instantes verás que en la consola aparecerá el output de los tres contenedores, lo cual facilita muchísimo la depuración.
Para crear algunos topics a través de la API puedes utilizar un cliente como Postman y hacer algunas peticiones POST a http://localhost:8080/api/topics pasando como parámetro name, que es el nombre del topic.
Para acceder al front end accede a http://localhost:4000, que es el puerto que has mapeado en el archivo docker-compose.yml.
Y lo mejor: cuando pulsas Control + C automáticamente apaga todos los contendores definidos en el archivo docker-compose.yml.
Como ves, Docker no solo te permite crear un contenedor para tu aplicación, sino que además te permite crear aplicaciones distribuidas de una forma muy útil para el desarrollo y la automatización.
El código lo tienes en mi GitHub.
Imagen de portada por kyohei ito.
¡Saludos!