Cómo utilizar Node.js con Docker – CodesCode

Aprende sobre los beneficios de ejecutar aplicaciones de Node.js en contenedores Docker y cómo crear un flujo de trabajo práctico para el desarrollo.

Este tutorial explica los beneficios de ejecutar aplicaciones Node.js en contenedores Docker y cómo crear un flujo de trabajo de desarrollo práctico.

Node.js te permite crear aplicaciones web rápidas y escalables utilizando JavaScript tanto en el servidor como en el cliente. Tu aplicación puede funcionar perfectamente en tu máquina de desarrollo, pero ¿puedes asegurarte de que funcionará en los dispositivos de tus compañeros o en los servidores de producción?

Considera estos escenarios:

  • Puedes estar usando macOS mientras que otros usan Windows y el servidor ejecuta Linux.
  • Tienes Node.js 20 instalado, pero otros utilizan diferentes versiones de tiempo de ejecución.
  • Estás utilizando dependencias como bases de datos, que pueden tener diferencias o no estar disponibles en otras plataformas.
  • ¿Estás seguro de que tu nuevo código no puede hacer nada peligroso en otro sistema operativo (SO)?

Docker ofrece una solución

Docker ayuda a resolver esos problemas de “pero funciona en mi máquina” mencionados anteriormente. En lugar de instalar una aplicación localmente, la ejecutas en un entorno ligero y aislado similar a una máquina virtual conocido como contenedor.

Docker

Una máquina virtual real emula hardware de PC para que puedas instalar un sistema operativo. Docker emula un sistema operativo para que puedas instalar aplicaciones. Es típico instalar una aplicación por contenedor basado en Linux y conectarlos a través de una red virtual para que puedan comunicarse en puertos HTTP.

Las ventajas son:

  • Tu configuración de Docker puede emular un servidor Linux de producción o puedes implementarla utilizando contenedores.
  • Puedes descargar, instalar y configurar dependencias en cuestión de minutos.
  • Tu aplicación en contenedor se ejecuta de manera idéntica en todos los dispositivos.
  • Es más seguro. Tu aplicación podría afectar el sistema operativo de un contenedor, pero no afectará tu PC y puedes reiniciar desde cero en cuestión de segundos.

Con Docker, no es necesario instalar Node.js en tu PC o usar una opción de gestión de tiempo de ejecución como nvm.

Tu primer script

Instala Docker Desktop en Windows, macOS o Linux y luego crea un pequeño script llamado version.js con el siguiente código:

console.log(`Versión de Node.js: ${ process.version }`);

Si tienes Node.js instalado localmente, intenta ejecutar el script. Verás la siguiente salida si tenías instalada la versión 18:

$ node version.jsVersión de Node.js: v18.18.2

Ahora puedes ejecutar el mismo script dentro de un contenedor Docker. El siguiente comando utiliza la versión más reciente de soporte a largo plazo (LTS) de Node.js. Entra en el directorio del script y ejecútalo en macOS o Linux:

$ docker run --rm --name version \  -v $PWD:/home/node/app \  -w /home/node/app \  node:lts-alpine version.jsVersión de Node.js: v20.9.0

Los usuarios de Windows Powershell pueden utilizar un comando similar con las llaves {} alrededor de PWD:

> docker run --rm --name version -v ${PWD}:/home/node/app -w /home/node/app node:lts-alpine version.jsVersión de Node.js: v20.9.0

La primera ejecución puede tardar uno o dos minutos en ejecutarse mientras Docker descarga las dependencias. Las ejecuciones posteriores son instantáneas.

Vamos a probar una versión diferente de Node, como la última versión de la versión 21. En macOS o Linux:

$ docker run --rm --name version \  -v $PWD:/home/node/app \  -w /home/node/app \  node:21-alpine version.jsVersión de Node.js: v21.1.0

En Windows Powershell:

> docker run --rm --name version -v ${PWD}:/home/node/app -w /home/node/app node:21-alpine version.jsVersión de Node.js: v21.1.0

Recuerda que el script se está ejecutando dentro de un contenedor de Linux que tiene una versión específica de Node.js instalada.

Explicación de los argumentos

Para los curiosos, los argumentos del comando son:

  • docker run crea un nuevo contenedor a partir de una imagen, más sobre eso a continuación.

  • --rm elimina el contenedor cuando termina. No es necesario conservar los contenedores a menos que haya una buena razón para reiniciarlos nuevamente.

  • --name version asigna un nombre al contenedor para facilitar su gestión.

  • -v $PWD:/home/node/app (o -v ${PWD}:/home/node/app) monta un volumen. En este caso, el directorio actual en el PC host se monta dentro del contenedor en /home/node/app.

  • -w /home/node/app establece el directorio de trabajo de Node.js.

  • node:lts-alpine es la imagen, en este caso, la versión LTS de Node.js en ejecución en Alpine Linux. La imagen contiene el sistema operativo y los archivos necesarios para ejecutar una aplicación. Piensa en ello como una instantánea del disco. Puedes iniciar cualquier número de contenedores desde la misma imagen: todos ellos hacen referencia al mismo conjunto de archivos, por lo que cada contenedor requiere recursos mínimos.

  • version.js es el comando a ejecutar (desde el directorio de trabajo).

Las imágenes de Docker están disponibles en Docker Hub y están disponibles para aplicaciones y entornos de ejecución, incluido Node.js. Las imágenes a menudo están disponibles en varias versiones identificadas con una etiqueta como :lts-alpine, 20-bullseye-slim o simplemente latest.

Ten en cuenta que Alpine es una distribución de Linux pequeña con un tamaño de imagen base de alrededor de 5 MB. No contiene muchas bibliotecas, pero es suficiente para proyectos simples como los de este tutorial.

Ejecución de aplicaciones complejas

El script version.js anterior es simple y no contiene dependencias ni pasos de compilación. La mayoría de las aplicaciones de Node.js utilizan npm para instalar y gestionar módulos en un directorio node_modules. No puedes usar el comando anterior porque:

  • No puedes ejecutar npm en el PC host (puede que no tengas Node.js o la versión correcta instalada).
  • Algunos módulos requieren archivos binarios específicos de la plataforma. No puedes instalar un binario de Windows en el PC host y esperar que se ejecute en un contenedor de Linux.

La solución es crear tu propia imagen de Docker que contenga:

  • una versión adecuada del entorno de ejecución de Node.js
  • una versión instalada de tu aplicación con todos los módulos necesarios

La siguiente demostración crea una aplicación sencilla de Node.js utilizando el framework Express.js. Crea un nuevo directorio llamado simple y agrega un archivo package.json con el siguiente contenido:

{  "name": "simple",  "version": "1.0.0",  "description": "simple Node.js and Docker example",  "type": "module",  "main": "index.js",  "scripts": {    "debug": "node --watch --inspect=0.0.0.0:9229 index.js",    "start": "node index.js"  },  "license": "MIT",  "dependencies": {    "express": "^4.18.2"  }}

Agrega un archivo index.js con código JavaScript:

// Aplicación Express
import express from 'express';
// Configuración
const cfg = {  port: process.env.PORT || 3000};
// Inicializar Express
const app = express();
// Ruta de la página de inicio
app.get('/:name?', (req, res) => {  res.send(`¡Hola ${ req.params.name || 'Mundo' }!`);});
// Iniciar el servidor
app.listen(cfg.port, () => {  console.log(`servidor escuchando en http://localhost:${ cfg.port }`);});

¡No intentes instalar dependencias ni ejecutar esta aplicación en la PC host!

Crea un archivo llamado Dockerfile con el siguiente contenido:

# Imagen base de Node.js LTS
FROM node:lts-alpine
# Definir variables de entorno
ENV HOME=/home/node/app
ENV NODE_ENV=production
ENV NODE_PORT=3000
# Crear la carpeta de la aplicación y asignar derechos al usuario node
RUN mkdir -p $HOME && chown -R node:node $HOME
# Establecer el directorio de trabajo
WORKDIR $HOME
# Establecer el usuario activo
USER node
# Copiar package.json desde el host
COPY --chown=node:node package.json $HOME/
# Instalar los módulos de la aplicación
RUN npm install && npm cache clean --force
# Copiar los archivos restantes
COPY --chown=node:node . .
# Exponer el puerto en el host
EXPOSE $NODE_PORT
# Comando para iniciar la aplicación
CMD [ "node", "./index.js" ]

Esto define los pasos necesarios para instalar y ejecutar tu aplicación. Nota que package.json es copiado a la imagen, luego se ejecuta npm install antes de copiar los archivos restantes. Esto es más eficiente que copiar todos los archivos de una vez, porque Docker crea una capa de imagen en cada comando. Si los archivos de tu aplicación (index.js) cambian, Docker solo necesita ejecutar los tres últimos pasos; no necesita volver a ejecutar npm install.

Opcionalmente, puedes agregar un archivo .dockerignore. Es similar a .gitignore y evita que se copien archivos innecesarios en la imagen mediante COPY . .. Por ejemplo:

Dockerfile.git.gitignore.vscodenode_modulesREADME.md

Construye una imagen de Docker llamada simple ingresando el siguiente comando (nota el punto . al final, que indica que estás utilizando los archivos en el directorio actual):

$ docker image build -t simple .

La imagen debería construirse en pocos segundos si la imagen de Docker node:lts-alpine utilizada anteriormente no ha sido eliminada de tu sistema.

Suponiendo que la compilación sea exitosa, inicia un contenedor a partir de tu imagen:

$ docker run -it --rm --name simple -p 3000:3000 simple
servidor escuchando en http://localhost:3000

El parámetro -p 3000:3000 publica o expone un <host-puerto> a un <contenedor-puerto>, por lo que el puerto 3000 en tu PC host se enruta al puerto 3000 dentro del contenedor.

Abre un navegador e ingresa la URL http://localhost:3000/ para ver “¡Hola Mundo!”

Prueba agregando nombres a la URL, como http://localhost:3000/Craig, para ver mensajes alternativos.

Finalmente, detén la ejecución de tu aplicación haciendo clic en el icono de detener en la pestaña Containers de Docker Desktop, o ingresa el siguiente comando en otra ventana de terminal:

docker container stop simple

Un Mejor Flujo de Trabajo de Desarrollo con Docker

El proceso anterior tiene algunas desventajas frustrantes:

  • Cualquier cambio en tu código (en index.js) requiere detener el contenedor, reconstruir la imagen, reiniciar el contenedor y volver a probar.

  • No puedes adjuntar un depurador de Node.js como el disponible en VS Code.

Docker puede mejorar tu flujo de trabajo de desarrollo al mantener la imagen existente de nivel de producción, pero ejecutar un contenedor con reemplazos para hacer lo siguiente:

  • Establecer variables de entorno como NODE_ENV a development.

  • Montar el directorio local dentro del contenedor.

  • Iniciar la aplicación con npm run debug. Esto ejecuta node --watch --inspect=0.0.0.0:9229 index.js, que reinicia la aplicación cuando los archivos cambian (nuevo en Node.js 18) y inicia el depurador con permisos para solicitudes externas al contenedor.

  • Expone el puerto de la aplicación 3000 y el puerto del depurador 9229 en el host.

Puedes hacer esto con un solo comando docker run muy largo, pero prefiero usar Docker Compose. Viene instalado con Docker Desktop y generalmente se utiliza para iniciar más de un contenedor. Crea un archivo nuevo llamado docker-compose.yml con el siguiente contenido:

version: '3'services:  simple:    environment:      - NODE_ENV=development    build:      context: ./      dockerfile: Dockerfile    container_name: simple    volumes:      - ./:/home/node/app    ports:      - "3000:3000"      - "9229:9229"    command: /bin/sh -c 'npm install && npm run debug'

Inicia tu aplicación en modo de depuración con el siguiente comando:

$ docker compose up[+] Building 0.0s[+] Running 2/2 ✔ Network simple_default  Created ✔ Container simple        CreatedAttaching to simplesimple  |simple  | up to date, audited 63 packages in 481mssimple  |simple  | > [email protected] debugsimple  | > node --watch --inspect=0.0.0.0:9229 index.jssimple  |simple  | Debugger listening on ws://0.0.0.0:9229/de201ceb-5d00-1234-8692-8916f5969cbasimple  | Para obtener ayuda, consulta: https://nodejs.org/en/docs/inspectorsimple  | server listening at http://localhost:3000

Ten en cuenta que las versiones antiguas de Docker Compose son scripts de Python que se ejecutan con docker-compose. Las versiones más nuevas tienen la funcionalidad de Compose integrada en el ejecutable principal, por lo que se ejecutan con docker compose.

Reinicios de la aplicación en vivo

Abre index.js, realiza un cambio (por ejemplo, en la cadena de la línea 14) y guarda el archivo para ver cómo la aplicación se reinicia automáticamente:

simple  | Reiniciando 'index.js'simple  | Debugger listening on ws://0.0.0.0:9229/acd16665-1399-4dbc-881a-8855ddf9d34csimple  | Para obtener ayuda, consulta: https://nodejs.org/en/docs/inspectorsimple  | server listening at http://localhost:3000

Abre o actualiza tu navegador en https://localhost:3000/ para ver la actualización.

Depuración con VS Code

Abre el panel Ejecutar y depurar de VS Code y haz clic en crear un archivo launch.json.

Panel de Ejecutar y depurar de VS Code

Elige Node.js en el menú desplegable y se creará y abrirá en el editor un archivo .vscode/launch.json. Agrega el siguiente código, que conecta el depurador al contenedor en ejecución:

{  // Use IntelliSense to learn about possible attributes.  // Hover to view descriptions of existing attributes.  // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387  "version": "0.2.0",  "configurations": [    {      "type": "node",      "request": "attach",      "name": "Attach to Container",      "address": "localhost",      "port": 9229,      "localRoot": "${workspaceFolder}",      "remoteRoot": "/home/node/app",      "skipFiles": [        "<node_internals>/**"      ]    }  ]}

Guarda el archivo y luego haz clic en Attach to Container en la parte superior del panel de depuración para iniciar la depuración.

Ejecutar y depurar de VS Code

Aparecerá una barra de herramientas de depuración. Cambia a index.js y agrega un punto de interrupción en la línea 14 haciendo clic en el margen izquierdo para mostrar un punto rojo.

Establecer un punto de interrupción en VS Code

Actualiza https://localhost:3000/ en tu navegador y VS Code detendrá la ejecución en el punto de interrupción y mostrará el estado de todas las variables de la aplicación. Haz clic en un icono en la barra de herramientas de depuración para seguir ejecutando, pasar por el código o desconectar el depurador.

Detener el contenedor

Detén el contenedor en ejecución abriendo otra terminal. cd al directorio de la aplicación y escribe lo siguiente:

docker compose down

Resumen

Aunque Docker requiere algo de tiempo inicial de configuración, los beneficios a largo plazo de un código resistente y distribuible superan el esfuerzo. Docker se vuelve invaluable cuando se agregan dependencias adicionales como bases de datos.

Este tutorial explica los conceptos básicos de cómo ejecutar aplicaciones Node.js en contenedores Docker. Para profundizar, considera estos recursos de CodesCode:

Comparte este artículo


Leave a Reply

Your email address will not be published. Required fields are marked *