Disponer en contenedor una aplicación de Node.js para el desarrollo con Docker Compose

Introducción

Si desarrolla activamente una aplicación, usando Docker, puede simplificar su flujo de trabajo y el proceso de implementación de su aplicación para producción. El trabajo con contenedores en tareas de desarrollo tiene los siguientes beneficios:

  • Los entornos son uniformes, lo cual significa que puede elegir los lenguajes y las dependencias que desee para su proyecto sin tener que preocuparse por posibles conflictos del sistema.
  • Los entornos están aislados. Esto facilita la resolución de problemas y la admisión de nuevos miembros del equipo.
  • Los entornos son portátiles; esto permite empaquetar y compartir su código con otros.

A través de este tutorial, verá la forma de configurar un entorno de desarrollo para una aplicación de Node.js usando Docker. Creará dos contenedores: uno para la aplicación de Node y otro para la base de datos de MongoDB, con Docker Compose. Debido a que esta aplicación funciona con Node y MongoDB, nuestra configuración realizará lo siguiente:

  • Sincronizar el código de la aplicación del host con el código del contenedor para facilitar los cambios durante el desarrollo.
  • Garantizar que los cambios al código de la aplicación funcionen sin un reinicio.
  • Crear una base de datos protegida por nombre de usuario y contraseña para los datos de la aplicación.
  • Hacer que los datos sean persistentes.

Al finalizar este tutorial, contará con una aplicación de información sobre tiburones en funcionamiento en contenedores de Docker:

Completar la colección de tiburones

Requisitos previos

Para seguir este tutorial, necesitará lo siguiente:

  • Un servidor de desarrollo con Ubuntu 18.04, un usuario no root con privilegios sudo y un firewall activo. Para obtener información sobre cómo configurarlos, consulte esta guía de configuración inicial para servidores.
  • Docker instalado en su servidor, siguiendo los pasos 1 y 2 de Cómo instalar y usar Docker en Ubuntu 18.04.
  • Docker Compose instalado en su servidor conforme el paso 1 de Cómo instalar Docker Compose en Ubuntu 18.04.

Paso 1: Clonar el proyecto y modificar las dependencias

El primer paso para crear esta configuración será clonar el código del proyecto y modificar su archivo package.json, que incluye las dependencias del proyecto. Añadiremos nodemon a devDependencies del proyectoy especificaremos que lo utilizaremos durante el desarrollo. Ejecutar la aplicación con nodemon garantiza que se reiniciará automáticamente cuando realice cambios en su código.

Primero, clone el repositorio nodejs-mongo-mongoose desde la cuenta de GitHub de la comunidad de DigitalOcean. Este repositorio incluye el código de la configuración descrita en el artículo Cómo integrar MongoDB con su aplicación de Node, en el que se explica la manera de integrar una base de datos de MongoDB con una aplicación de Node existente usando Mongoose.

Clone el repositorio en un directorio llamado node_project:

  • git clone https://github.com/do-community/nodejs-mongo-mongoose.git node_project

Diríjase al directorio node_project:

  • cd node_project

Abra el archivo package.json del proyecto usando nano o su editor favorito:

  • nano package.json

Debajo de las dependencias del proyecto y encima de la llave de cierre, cree un nuevo objeto devDependencies que incluya nodemon:

~/node_project/package.json

... "dependencies": {     "ejs": "^2.6.1",     "express": "^4.16.4",     "mongoose": "^5.4.10"   },   "devDependencies": {     "nodemon": "^1.18.10"   }     } 

Guarde y cierre el archivo cuando haya terminado de editar.

Una vez que se implemente el código del proyecto y se modifiquen sus dependencias, podrá proceder a refactorizar el código para un flujo de trabajo en contenedor.

Paso 2: Configurar su aplicación para que funcione con contenedores

Modificar nuestra aplicación para un flujo de trabajo en contenedores implica hacer que nuestro código sea más modular. Los contenedores ofrecen portabilidad entre entornos, y nuestro código debería reflejar esto manteniendo un nivel de disociación lo más alto posible respecto del sistema operativo subyacente. A fin de lograr esto, refactorizaremos nuestro código para hacer un mayor uso de la propiedad process.env de Node, que muestra un objeto con información sobre su entorno de usuario en el tiempo de ejecución. Podemos usar este objeto en nuestro código para asignar de forma dinámica información de la configuración en el tiempo de ejecución con variables de entorno.

Comenzaremos con apps.js, nuestro punto de entrada principal para la aplicación. Abra el archivo:

  • nano app.js

Dentro, verá una definición para una constante port, además de una función listen que utiliza esta constante para especificar el puerto en el que la aplicación escuchará.

~/home/node_project/app.js

... const port = 8080; ... app.listen(port, function () {   console.log('Example app listening on port 8080!'); }); 

Redefiniremos la constante port para permitir la asignación dinámica en el tiempo de ejecución usando el objeto process.env. Realice los siguientes cambios en la definición de la constante y la función listen:

~/home/node_project/app.js

... const port = process.env.PORT || 8080; ... app.listen(port, function () {   console.log(`Example app listening on ${port}!`); }); 

Con nuestra nueva definición de la constante, port se asigna de forma dinámica usando el valor transmitido en el tiempo de ejecución o 8080. De modo similar, reescribimos la función listen para que use un literal de plantilla que interpolará el valor del puerto al escuchar conexiones. Debido a que asignaremos nuestros puertos en otra parte, estas revisiones evitarán que debamos revisar continuamente este archivo a medida que nuestro entorno cambie.

Una vez que finalice la edición, guarde y cierre el archivo.

A continuación, modificaremos la información de conexión de nuestra base de datos para eliminar cualquier credencial de configuración. Abra el archivo db.js, que contiene esta información:

  • nano db.js

Actualmente, el archivo hace lo siguiente:

  • Importa Mongoose, el asignador de objeto a documento (ODM) que usaremos para crear esquemas y modelos para los datos de nuestra aplicación.
  • Establece las credenciales de la base de datos como constantes, incluidos el nombre de usuario y la contraseña.
  • Establece conexión con la base de datos usando el método mongoose.connect.

Para obtener más información sobre el archivo, consulte el paso 3 de Cómo integrar MongoDB con su aplicación de Node.

Nuestro primer paso para modificar el archivo será redefinir las constantes que incluyan información confidencial. Actualmente, estas constantes tendrán este aspecto:

~/node_project/db.js

... const MONGO_USERNAME = 'sammy'; const MONGO_PASSWORD = 'your_password'; const MONGO_HOSTNAME = '127.0.0.1'; const MONGO_PORT = '27017'; const MONGO_DB = 'sharkinfo'; ... 

En vez de realizar una codificación rígida de esta información, puede usar el objeto process.env a fin de capturar los valores del tiempo de ejecución para estas constantes: Modifique el bloque para que tenga este aspecto:

~/node_project/db.js

... const {   MONGO_USERNAME,   MONGO_PASSWORD,   MONGO_HOSTNAME,   MONGO_PORT,   MONGO_DB } = process.env; ... 

Guarde y cierre el archivo cuando haya terminado de editar.

En este punto, habrá modificado db.jspara que funcione con las variables de entorno de su aplicación, pero aún necesita una forma de pasar estas variables a su aplicación. Crearemos un archivo .env con valores que pueda pasar a su aplicación en el tiempo de ejecución.

Abra el archivo:

  • nano .env

Este archivo incluirá la información que eliminó de db.js: el nombre de usuario y la contraseña para la base de su aplicación, además del ajuste del puerto y el nombre de la base de datos. Recuerde actualizar el nombre de usuario, la contraseña y el nombre de la base de datos que se muestran aquí con su propia información:

~/node_project/.env

MONGO_USERNAME=sammy MONGO_PASSWORD=your_password MONGO_PORT=27017 MONGO_DB=sharkinfo 

Tenga en cuenta que eliminamos el ajuste de host que originalmente aparecía en db.js. Ahora definiremos nuestro host a nivel del archivo de Docker Compose, junto con información adicional sobre nuestros servicios y contenedores.

Guarde y cierre este archivo cuando concluya la edición.

Debido a que su archivo .env contiene información confidencial, le convendrá asegurarse de que se incluya en los archivos .dockerignore y .gitignore de su proyecto para que no realice copias al control de su versión o a los contenedores.

Abra su archivo .dockerignore:

  • nano .dockerignore

Añada la siguiente línea a la parte inferior del archivo:

~/node_project/.dockerignore

... .gitignore .env 

Guarde y cierre el archivo cuando haya terminado de editar.

El archivo .gitignore de este repositorio ya incluye .env, pero verifique que esté allí:

  • nano .gitignore

~~/node_project/.gitignore

... .env ... 

En este punto, habrá extraído correctamente la información confidencial del código de su proyecto y tomado medidas para controlar la forma y la ubicación en que se copia esta información. Ahora, podrá aportar más solidez al código de conexión de su base de datos a fin de optimizarlo para un flujo de trabajo en contenedor.

Paso 3: Modificar ajustes de conexión de la base de datos

Nuestro siguiente paso será hacer que el método de conexión de nuestra base de datos sea más sólido añadiendo código que gestione los casos en los que nuestra aplicación no se conecte con nuestra base de datos. Sumar este nivel de resistencia al código de su aplicación es una práctica recomendada cuando se trabaja con contenedores usando Compose.

Abra db.js para editarlo:

  • nano db.js

Verá el código que añadimos antes, junto con la constante url para la URI de conexión de Mongo y el método connect de Mongoose:

~/node_project/db.js

... const {   MONGO_USERNAME,   MONGO_PASSWORD,   MONGO_HOSTNAME,   MONGO_PORT,   MONGO_DB } = process.env;  const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;  mongoose.connect(url, {useNewUrlParser: true}); 

Actualmente, nuestro método connect acepta una opción que indica a Mongoose que utilice el nuevo analizador de URL de Mongo. Añadiremos algunas opciones más a este método para definir parámetros para intentos de reconexión. Podemos hacer esto creando una constante options que incluya la información pertinente, además de usar la opción del nuevo analizador de URL. En sus constantes de Mongo, añada la siguiente definición para una constante options:

~/node_project/db.js

... const {   MONGO_USERNAME,   MONGO_PASSWORD,   MONGO_HOSTNAME,   MONGO_PORT,   MONGO_DB } = process.env;  const options = {   useNewUrlParser: true,   reconnectTries: Number.MAX_VALUE,   reconnectInterval: 500,   connectTimeoutMS: 10000, }; ... 

La opción reconnectTries indica a Mongoose que siga intentando establecer conexión indefinidamente, mientras que reconnectInterval define el período entre los intentos de conexión en milisegundos. connectTimeoutMS define 10 segundos como el período que el controlador de Mongo esperará antes de que falle el intento de conexión.

Ahora podemos usar la nueva constante options en el método connect de Mongoose para ajustar nuestra configuración de conexión de Mongoose. También añadiremos una promesa para manejar los posibles errores de conexión.

En este momento, el método connect de Mongoose tiene este aspecto:

~/node_project/db.js

... mongoose.connect(url, {useNewUrlParser: true}); 

Elimine el método connect existente y sustitúyalo por el siguiente código, que incluye la constante options y una promesa:

~/node_project/db.js

... mongoose.connect(url, options).then( function() {   console.log('MongoDB is connected'); })   .catch( function(err) {   console.log(err); }); 

En caso de que la conexión se realice correctamente, nuestra función registrará un mensaje correspondiente; de lo contrario, aplicará catch al error y lo registrará para que podamos resolverlo.

El archivo terminado tendrá este aspecto:

~/node_project/db.js

const mongoose = require('mongoose');  const {   MONGO_USERNAME,   MONGO_PASSWORD,   MONGO_HOSTNAME,   MONGO_PORT,   MONGO_DB } = process.env;  const options = {   useNewUrlParser: true,   reconnectTries: Number.MAX_VALUE,   reconnectInterval: 500,   connectTimeoutMS: 10000, };  const url = `mongodb://${MONGO_USERNAME}:${MONGO_PASSWORD}@${MONGO_HOSTNAME}:${MONGO_PORT}/${MONGO_DB}?authSource=admin`;  mongoose.connect(url, options).then( function() {   console.log('MongoDB is connected'); })   .catch( function(err) {   console.log(err); }); 

Guarde y cierre el archivo cuando concluya la edición.

Con esto, habrá añadido resistencia al código de su aplicación para gestionar los casos en los cuales fuera posible que esta no pudiera establecer conexión con su base de datos. Una vez establecido este código, puede proceder a definir sus servicios con Compose.

Paso 4: Definir servicios con Docker Compose

Una vez refactorizado su código, estará listo para escribir el archivo docker-compose.yml con las definiciones de su servicio. Un servicio en Compose es un contenedor en ejecución y las definiciones del servicio, que incluirá en su archivo docker-compose.yml, contienen información sobre cómo se ejecutará cada imagen del contenedor. La herramienta Compose le permite definir varios servicios para crear aplicaciones en diferentes contenedores.

Antes de definir nuestros servicios, sin embargo, añadiremos una herramienta a nuestro proyecto llamada wait-for para garantizar que nuestra aplicación solo intente establecer conexión con nuestra base de datos una vez que las tareas de inicio de esta última se completen. Esta secuencia de comandos utiliza netcat para determinar, mediante un sondeo, si un host y puerto específicos aceptan conexiones TCP o no. Usarla le permite controlar los intentos que su aplicación realiza para establecer conexión con su base de datos determinando, mediante una prueba, si la base de datos está lista o no para aceptar conexiones.

Aunque Compose le permite especificar dependencias entre los servicios usando la opción depends_on, esta orden se basa más en el hecho que el contenedor se ejecute o no que en el hecho de que esté preparado. Usar depends_on no será la mejor opción para nuestra configuración, pues queremos que nuestra aplicación se conecte solo cuando las se completen tareas de la base de datos, incluida la de añadir usuario y contraseña a la base de datos de autenticación de admin. Para obtener más información sobre cómo usar wait-for y otras herramientas para controlar la orden de inicio, consulte las recomendaciones pertinentes en la documentación de Compose.

Abra un archivo llamado wait-for.sh:

  • nano wait-for.sh

Pegue el siguiente código en el archivo para crear la función de sondeo:

~/node_project/app/wait-for.sh

#!/bin/sh  # original script: https://github.com/eficode/wait-for/blob/master/wait-for  TIMEOUT=15 QUIET=0  echoerr() {   if [ "$QUIET" -ne 1 ]; then printf "%sn" "$*" 1>&2; fi }  usage() {   exitcode="$1"   cat << USAGE >&2 Usage:   $cmdname host:port [-t timeout] [-- command args]   -q | --quiet                        Do not output any status messages   -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout   -- COMMAND ARGS                     Execute command with args after the test finishes USAGE   exit "$exitcode" }  wait_for() {   for i in `seq $TIMEOUT` ; do     nc -z "$HOST" "$PORT" > /dev/null 2>&1      result=$?     if [ $result -eq 0 ] ; then       if [ $# -gt 0 ] ; then         exec "[email protected]"       fi       exit 0     fi     sleep 1   done   echo "Operation timed out" >&2   exit 1 }  while [ $# -gt 0 ] do   case "$1" in     *:* )     HOST=$(printf "%sn" "$1"| cut -d : -f 1)     PORT=$(printf "%sn" "$1"| cut -d : -f 2)     shift 1     ;;     -q | --quiet)     QUIET=1     shift 1     ;;     -t)     TIMEOUT="$2"     if [ "$TIMEOUT" = "" ]; then break; fi     shift 2     ;;     --timeout=*)     TIMEOUT="${1#*=}"     shift 1     ;;     --)     shift     break     ;;     --help)     usage 0     ;;     *)     echoerr "Unknown argument: $1"     usage 1     ;;   esac done  if [ "$HOST" = "" -o "$PORT" = "" ]; then   echoerr "Error: you need to provide a host and port to test."   usage 2 fi  wait_for "[email protected]" 

Guarde y cierre el archivo cuando termine de añadir el código.

Haga que la secuencia de comandos sea ejecutable:

  • chmod +x wait-for.sh

A continuación, abra el archivo docker-compose.yml:

  • nano docker-compose.yml

Primero defina el servicio de la aplicación nodejs agregando el siguiente código al archivo:

~/node_project/docker-compose.yml

version: '3'  services:   nodejs:     build:       context: .       dockerfile: Dockerfile     image: nodejs     container_name: nodejs     restart: unless-stopped     env_file: .env     environment:       - MONGO_USERNAME=$MONGO_USERNAME       - MONGO_PASSWORD=$MONGO_PASSWORD       - MONGO_HOSTNAME=db       - MONGO_PORT=$MONGO_PORT       - MONGO_DB=$MONGO_DB     ports:       - "80:8080"     volumes:       - .:/home/node/app       - node_modules:/home/node/app/node_modules     networks:       - app-network     command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js 

La definición del servicio nodejs incluye las siguientes opciones:

  • build: define las opciones de configuración, incluido el context y dockerfile, que se aplicarán cuando Compose cree la imagen de la aplicación. Si desea utilizar una imagen existente de un registro como Docker Hub, podría utilizar la instrucción image como alternativa, con información sobre su nombre de usuario, repositorio y etiqueta de imagen.
  • context: esto define el contexto de compilación para la compilación de la imagen; en este caso, el directorio del proyecto actual.
  • dockerfile: esto especifica el Dockerfile del directorio actual de su directorio como el archivo que Compose usará para complilar la imagen de la aplicación. Para obtener más información sobre este archivo, consulte Cómo crear una aplicación de Node.js con Docker.
  • image y container_name: aplican nombres a la imagen y al contenedor.
  • restart: define la política de reinicio. El valor predeterminado es no, pero configuramos el contenedor para reiniciarse a menos que se detenga.
  • env_file: indica a Compose que deseamos añadir variables de entorno de un archivo llamado .env, ubicado en nuestro contexto de compilación.
  • environment: esta opción le permite añadir los ajustes de conexión de Mongo que definió en el archivo .env. Tenga en cuenta que no fijaremos NODE_ENV en development, ya que éste es el comportamiento predeterminado de Express si NODE_ENV no se configura. Cuando procedamos con la producción, podrá fijarlo en production para permitir el almacenamiento de vistas en caché y recibir menos mensajes de error confusos. Observe, además, que especificamos el contenedor de la base de datos db como host, como se explicó en el paso 2.
  • ports: asigna el puerto 80 del host al puerto 8080 del contenedor.
  • volumes: incluiremos dos tipos de montajes aquí:
    • El primero es un montaje de enlace, que monta el código de nuestra aplicación del host en el directorio /home/node/app del contenedor. Esto facilitará un desarrollo rápido, ya que cualquier cambio que realice a su código de host se completará de inmediato en el contenedor.
    • El segundo es un volumen con nombre: node_modules. Cuando Docker ejecute la instrucción npm install que se indica en el Dockerfile de la aplicación, npm creará en el contenedor un nuevo directorio node_modules en el que se incluirán los paquetes necesarios para ejecutar la aplicación. El montaje de enlace que acabamos de crear ocultará, sin embargo, este directorio node_modules recién creado. Debido a que node_modules en el host está vacío, el bind asignará un directorio vacío al contenedor, con lo cual se anulará el nuevo directorio node_modules y se evitará el inicio de nuestra aplicación. El volumen llamado node_modules resuelve este problema haciendo que persista el contenido del directorio /home/node/app/node_modules y montándolo en el contenedor, con lo cual se ocultará el enlace.

Tenga en cuenta lo siguiente cuando utilice este enfoque:

  • Su enlace montará los contenidos del directorio node_modules del contenedor en el host y este directorio será propiedad de root, ya que el volumen nombrado fue creado por Docker.
  • Si dispone de un directorio node_modules preexistente en el host, anulará el directorio node_modules creado en el contenedor. Para configuración que estamos creando en este tutorial, se supone que no dispone de un directorio node_modules preexistente y que no trabajará con npm en su host. Esto se ajusta a un enfoque de doce factores para el desarrollo de la aplicación, que minimiza las dependencias entre los entornos de ejecución.

    • networks: especifica que nuestro servicio de aplicación se unirá a la red app-network, que definiremos al final del archivo.
    • command: esta opción le permite establecer el comando que debería ejecutarse cuando Compose ejecute la imagen. Tenga en cuenta que con esto se anulará la instrucción CMD que establecimos en el Dockerfile de nuestra aplicación. En este caso, ejecutaremos la aplicación usando la secuencia de comandos wait-for que sondeará el servicio db en el puerto 27017 para probar si el servicio de la base de datos está listo o no. Una vez que esta prueba se realice, la secuencia de comandos ejecutará el comando que establecimos, /home/node/app/node_modules/.bin/nodemon app.js, para iniciar la aplicación con nodemon. Esto garantizará que cualquier cambio futuro que realicemos en nuestro código se recargue sin que debamos reiniciar la aplicación.

A continuación, cree el servicio db agregando el siguiente código debajo de la definición del servicio de la aplicación:

~/node_project/docker-compose.yml

...   db:     image: mongo:4.1.8-xenial     container_name: db     restart: unless-stopped     env_file: .env     environment:       - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME       - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD     volumes:         - dbdata:/data/db        networks:       - app-network   

Algunos de los ajustes que definimos para el servicio nodejs seguirán siendo los mismos, pero también realizamos los siguientes cambios en las definiciones de image, environment y volumes:

  • image: para crear este servicio, Compose extraerá la imagen de Mongo 4.1.8-xenial de Docker Hub. Fijaremos una versión concreta para evitar posibles conflictos futuros a medida que la imagen de Mongo cambie. Para obtener más información sobre la fijación de la versiones, consulte la documentación de Docker en Prácticas recomendadas de Dockerfile.
  • MONGO_INITDB_ROOT_USERNAME, MONGO_INITDB_ROOT_PASSWORD: la imagen de mongo pone estas variables de entorno a disposición para que pueda modificar la inicialización de la instancia de su base de datos. MONGO_INITDB_ROOT_USERNAME y MONGO_INITDB_ROOT_PASSWORD crean juntos un usuario root en la base de datos de autenticación admin y verifican que la autenticación esté habilitada cuando se inicie el contenedor. Configuramos MONGO_INITDB_ROOT_USERNAME y MONGO_INITDB_ROOT_PASSWORD usando los valores de nuestro archivo .env, que pasamos al servicio db usando la opción env_file. Hacer esto significa que nuestro usuario de la aplicación sammy será un usuario root en la instancia de la base de datos, con acceso a todos los privilegios administrativos y operativos de esa función. Cuando trabaje en producción, le convendrá crear un usuario de aplicación dedicado con privilegios de alcance correspondiente. Nota: Tenga en cuenta que estas variables no se aplicarán si inicia el contenedor con un directorio de datos existente implementado.
  • dbdata:/data/db: el volumen llamado dbdata preservará los datos almacenados en /data/db, el directorio de datos predeterminado de Mongo. Esto garantizará que no pierda datos en los casos en los que detenga o elimine contenedores.

También agregamos el servcio db a la red app-network con la opción networks.

Como paso final, añada las definiciones de volumen y red al final del archivo:

~/node_project/docker-compose.yml

... networks:   app-network:     driver: bridge  volumes:   dbdata:   node_modules:   

La red de puente definida por el usuario app-network permite la comunicación entre nuestros contenedores, ya que están en el mismo host de demonio de Docker. Esto agiliza el tráfico y la comunicación dentro de la aplicación, ya que abre todos los puertos entre contenedores en la misma red de puente y, al mismo tiempo, no expone ningún puerto al exterior. Por lo tanto, nuestros contenedores db y nodejs pueden comunicarse entre sí y solo debemos exponer el puerto 80 para el acceso de front-end a la aplicación.

Nuestra clave volumes de nivel nivel superior define dbdata y node_modules de los volúmenes. Cuando Docker crea volúmenes, el contenido de estos se almacena en una parte del sistema de archivos host, /var/ib/docker/volume/, que Docker administra. El contenido de cada volumen se almacena en un directorio en /var/lib/docker/volume/ y se monta en cualquier contenedor que utilice el volumen. De esta forma, los datos de la información sobre tiburones que nuestros usuarios crearán persistirán en el volumen dbdata, incluso si eliminamos y volvemos a crear el contenedor db.

El archivo docker-compose.yml terminado tendrá este aspecto:

~/node_project/docker-compose.yml

version: '3'  services:   nodejs:     build:       context: .       dockerfile: Dockerfile     image: nodejs     container_name: nodejs     restart: unless-stopped     env_file: .env     environment:       - MONGO_USERNAME=$MONGO_USERNAME       - MONGO_PASSWORD=$MONGO_PASSWORD       - MONGO_HOSTNAME=db       - MONGO_PORT=$MONGO_PORT       - MONGO_DB=$MONGO_DB     ports:       - "80:8080"     volumes:       - .:/home/node/app       - node_modules:/home/node/app/node_modules     networks:       - app-network     command: ./wait-for.sh db:27017 -- /home/node/app/node_modules/.bin/nodemon app.js    db:     image: mongo:4.1.8-xenial     container_name: db     restart: unless-stopped     env_file: .env     environment:       - MONGO_INITDB_ROOT_USERNAME=$MONGO_USERNAME       - MONGO_INITDB_ROOT_PASSWORD=$MONGO_PASSWORD     volumes:            - dbdata:/data/db     networks:       - app-network    networks:   app-network:     driver: bridge  volumes:   dbdata:   node_modules:   

Guarde y cierre el archivo cuando haya terminado de editar.

Una vez implementadas las definiciones de su servicio, estará listo para iniciar la aplicación.

Paso 5: Probar la aplicación

Una vez implementado su archivo docker-compose.yml, puede crear sus servicios con el comando docker-compose up. También puede comprobar que sus datos persisitirán deteniendo y eliminando sus contenedores con docker-compose down.

Primero, compile las imágenes del contenedor y cree los servicios ejecutando docker-compose up con el indicador -d, que luego ejecutará los contenedores nodejs y db en segundo plano.

  • docker-compose up -d

Verá un resultado que confirmará la creación de sus servicios:

Output... Creating db ... done Creating nodejs ... done 

También puede obtener información más detallada sobre los procesos de inicio mostrando el resultado del registro de los servicios:

  • docker-compose logs

Si todo se inició de forma correcta, verá algo similar a esto:

Output... nodejs    | [nodemon] starting `node app.js` nodejs    | Example app listening on 8080! nodejs    | MongoDB is connected ... db        | 2019-02-22T17:26:27.329+0000 I ACCESS   [conn2] Successfully authenticated as principal sammy on admin 

También puede verificar el estado de sus contenedores con docker-compose ps:

  • docker-compose ps

Verá un resultado que indicará que sus contenedores están en ejecución:

Output Name               Command               State          Ports         ---------------------------------------------------------------------- db       docker-entrypoint.sh mongod      Up      27017/tcp            nodejs   ./wait-for.sh db:27017 --  ...   Up      0.0.0.0:80->8080/tcp 

Una vez que los servicios estén en ejecución, podrá visitar http://your_server_ip en el navegador. Verá una página de aterrizaje similar a esta:

Página de destino de la aplicación

Haga clic en el botón Get Shark Info. Verá una página con un formulario de entrada en el que podrá introducir un nombre del tiburón y una descripción del carácter general de este:

Formulario de Información sobre tiburones

En el formulario, agregue un tiburón que elija. A los efectos de esta demostración, añadiremos Megalodon Shark en el campo Shark Name y Ancient en el campo Shark Character:

Formulario de tiburones completado

Haga clic en el botón Submit. Visualizará una página con la siguiente información sobre tiburones que se le mostrará de nuevo:

Resultado de tiburones

Como paso final, podemos hacer una prueba para verificar que los datos que acaba de introducir persistan si elimina el contenedor de su base de datos.

Cuando regrese a su terminal, escriba el siguiente comando para detener y eliminar sus contenedores y su red:

  • docker-compose down

Tenga en cuenta que no incluiremos la opción --volumes; por lo tanto, no se eliminará nuestro volumen dbdata.

El siguiente resultado confirma que se eliminaron sus contenedores y su red:

OutputStopping nodejs ... done Stopping db     ... done Removing nodejs ... done Removing db     ... done Removing network node_project_app-network 

Vuelva a crear los contenedores:

  • docker-compose up -d

Ahora, vuelva al formulario de información de tiburones:

Formulario de información  sobre tiburones

Introduzca un nuevo tiburón que elija. Usaremos Whale Shark y Large:

Introduzca un nuevo tiburón

Una vez que haga clic en Submit, verá que se agregó el nuevo tiburón a la colección de tiburones de su base de datos sin pérdida de datos que ya introdujo:

Colección completa de tiburones

Su aplicación ahora se ejecuta en los contenedores de Docker con la persistencia de datos y la sincronización de códigos habilitadas.

Conclusión

Siguiendo este tutorial, creó una configuración de desarrollo para su aplicación de Node usando contenedores de Docker. Hizo que su proyecto fuera más modular y portátil mediante la extracción de información confidencial y la desvinculación del estado de su aplicación del código de esta. También configuró un archivo docker-compose.yml estándar que podrá revisar a medida que sus necesidades y requisitos de desarrollo cambien.

A medida que realice desarrollos, es posible que le interese aprender más sobre cómo diseñar aplicaciones para flujos de trabajo en contenedores y de Cloud Native. Consulte Crear aplicaciones para Kubernetes y Modernizar aplicaciones para Kubernetes para obtener más información sobre estos temas.

Para obtener más información sobre el código utilizado en este tutorial, consulte Cómo crear una aplicación de Node.js con Docker y Cómo integrar MongoDB con su aplicación de Node. Para obtener información sobre cómo implementar una aplicación de Node con un proxy inverso de Nginx usando contenedores, consulte Cómo proteger una aplicación de Node.js en contenedor con Nginx, Let´s Encrypt y Docker Compose.