Desarrollo10 de marzo de 2026

Docker para Node.js en desarrollo y en producción

Nuestro stack principal es Node.js/JavaScript y, como muchos equipos, hemos llevado los entornos de desarrollo y producción a contenedores Docker. Sin embargo, al empezar a aprender Docker, vimos que la mayoría de artículos se centraban o bien en desarrollo o bien en producción, y no había nada sobre cómo organizar la configuración de Docker para que fuera flexible en ambos casos.

En este artículo te enseñamos distintos casos de uso y ejemplos de Dockerfiles para Node.js, explicamos el porqué de cada decisión y te ayudamos a imaginar cómo debería ser tu flujo de trabajo con Docker. Empezamos con un ejemplo sencillo y después pasamos a escenarios más complejos y a soluciones para que tu experiencia de desarrollo sea coherente, uses Docker o no.

Aviso: esta guía es larga y está pensada para públicos con distintos niveles de Docker; en algunos momentos las instrucciones te parecerán obvias, pero intentaremos añadir junto a ellas algunos puntos relevantes para que tengas una visión completa de la configuración final.

Requisitos previos

Casos que se explican

  • Dockerfile y docker-compose básicos para Node.js
  • Nodemon en desarrollo, Node en producción
  • Mantener las devDependencies fuera de la imagen Docker de producción
  • Usar un build multi-stage para imágenes que necesitan node-gyp

Añade un archivo .dockerignore

Antes de configurar el Dockerfile, añade un archivo .dockerignore a la carpeta de tu app. El archivo .dockerignore excluye de los comandos COPY/ADD los archivos que se indican en él. Más información aquí


node_modules
npm-debug.log
Dockerfile*
docker-compose*
.dockerignore
.git
.gitignore
README.md
LICENSE
.vscode

Dockerfile básico para Node.js

Para que quede todo claro, empezaremos con un Dockerfile básico que puedes usar en proyectos sencillos de Node.js. Por sencillo nos referimos a que tu código no tiene dependencias nativas adicionales ni lógica de build.


FROM node:10-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "npm", "start" ]

Encontrarás algo parecido en cualquier artículo sobre Docker y Node.js. Vamos a repasarlo brevemente.


WORKDIR / usr / src / app;

El workdir es una especie de directorio por defecto que se usa en las instrucciones RUN, CMD, ENTRYPOINT, COPY y ADD. En algunos artículos verás que se hace mkdir /app y luego se define como workdir, pero no es una buena práctica. Usa una carpeta ya existente, /usr/src/app, que es más adecuada para esto.


COPY package*.json ./
RUN npm install

Otra buena práctica: copia package.json y package-lock.json antes de copiar el código en el contenedor. Docker guardará en cache los node_modules instalados como una capa independiente; así, si cambias el código de la app y ejecutas el build, los node_modules no se volverán a instalar si no has tocado package.json. En general, aunque olvides añadir esas líneas, no tendrás muchos problemas. Normalmente solo necesitarás ejecutar docker build cuando cambie package.json, lo que implica instalar desde cero de todos modos. En el resto de casos, en desarrollo no sueles ejecutar docker build muy a menudo después del primer build.

Aquí entra en juego docker-compose

Antes de ejecutar la app en producción, hay que desarrollarla. La mejor forma de orquestar y ejecutar tu entorno Docker es con docker-compose. Defines en un archivo YAML, con una sintaxis sencilla, la lista de contenedores/servicios que quieres ejecutar y sus instrucciones.


version: '3'
services:
example-service:
- build: .
- volumes:
- - .:/usr/src/app
- - /usr/src/app/node_modules
- ports:
- - 3000:3000
- - 9229:9229
- command: npm start

En este ejemplo de configuración básica de docker-compose.yaml, el build se hace con el Dockerfile de la carpeta de la app; después, esa carpeta se monta en el contenedor y los node_modules instalados dentro del contenedor durante el build no se sobrescriben con los de tu carpeta local. El puerto 3000 se expone a tu localhost, suponiendo que tengas un servidor web en marcha. El 9229 sirve para exponer el puerto de depuración. Más información aquí.

Ahora ejecuta tu app con:


docker-compose up

O usa la extensión de VS Code para lo mismo.

Con este comando exponemos a localhost los puertos 3000 y 9229 de la app dockerizada, montamos la carpeta actual con la app en /usr/src/app y usamos un pequeño truco para que Docker no sobrescriba los node modules con los de la máquina local.

Entonces, ¿puedes usar ese Dockerfile en desarrollo y en producción?

Sí y no.

Diferencias en CMD

Para empezar, en desarrollo lo normal es que quieras que la app se recargue cuando cambia un archivo. Para eso puedes usar nodemon. Pero en producción quieres ejecutarla sin él. Eso significa que el CMD (comando) de desarrollo y el de producción tienen que ser distintos.

Hay varias opciones para conseguirlo:

  1. Sustituir el CMD por el comando que ejecuta la app sin nodemon, que puede ser un comando definido aparte en tu package.json, por ejemplo:


    "scripts": {
    "start": "nodemon --inspect=0.0.0.0 src/index.js",
    "start:prod": "node src/index.js"
    }

    En ese caso, tu Dockerfile podría quedar así:


    FROM node:10-alpine
    WORKDIR /usr/src/app
    COPY package*.json ./
    RUN npm install
    COPY . .
    CMD [ "npm", “run”, "start:prod" ]

    Sin embargo, como en desarrollo usas el archivo docker-compose, dentro puedes definir un comando distinto, igual que en el ejemplo anterior:


    version: '3'
    services:
    ### ... previous instructions
    - command: npm start

  2. Si las diferencias son mayores o usas docker-compose tanto en desarrollo como en producción, puedes crear varios archivos docker-compose o Dockerfile según lo que cambie. Por ejemplo, docker-compose.dev.yml o Dockerfile.dev.

Gestionar la instalación de paquetes

Por lo general, conviene que la imagen de producción sea lo más ligera posible y no instalar dependencias de node modules que no hacen falta en producción. Se puede resolver manteniendo un único Dockerfile.

Revisa tu package.json y separa las devDependencies de las dependencies. Más información aquí. En resumen, si ejecutas npm install con el flag --production o defines NODE_ENV como production, no se instalará ninguna devDependency. Añadiremos unas líneas al Dockerfile para gestionarlo:


FROM node:10-alpine
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
CMD [ "npm", “run”, "start:prod" ]

Para personalizar el comportamiento usamos


ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}

Docker permite pasar argumentos de build desde el comando docker o desde docker-compose. Se usará NODE_ENV=development por defecto mientras no lo sobrescribamos con otro valor. Aquí encontrarás una buena explicación.

Ahora, cuando hagas el build de los contenedores con un archivo docker-compose, se instalarán todas las dependencias, y cuando lo hagas para producción podrás pasar production como argumento de build y se ignorarán las devDependencies. Si usas servicios de CI para construir los contenedores, basta con añadir esa opción a su configuración. Más información aquí

Build multi-stage para imágenes que necesitan node-gyp

No todas las apps que ejecutes en Docker usarán solo dependencias de JS; algunas necesitan node-gyp y librerías nativas adicionales instaladas en el sistema operativo.

Para resolverlo podemos usar builds multi-stage, que permiten instalar y compilar todas las dependencias en un contenedor aparte y pasar al contenedor final solo el resultado de la instalación, sin residuos. El Dockerfile podría quedar así:


// The instructions for the first stage
FROM node:10-alpine as builder
ARG NODE_ENV=development
ENV NODE_ENV=${NODE_ENV}
RUN apk --no-cache add python make g++
COPY package*.json ./
RUN npm install
// The instructions for second stage
FROM node:10-alpine
WORKDIR /usr/src/app
COPY --from=builder node_modules node_modules
COPY . .
CMD [ "npm", “run”, "start:prod" ]

En este ejemplo, en la primera fase instalamos y compilamos todas las dependencias según el entorno y, en la segunda, copiamos los node_modules que usaremos en desarrollo y en producción.

La línea RUN apk --no-cache add python make g++ puede variar de un proyecto a otro, ya que seguramente necesitarás otras dependencias.


COPY --from=builder node_modules node_modules

En esa línea copiamos la carpeta node_modules de la primera fase a la carpeta node_modules de la segunda. Como en la segunda fase definimos WORKDIR como /usr/src/app, los node_modules se copiarán en esa carpeta. Resumen

Esperamos que esta guía te haya ayudado a entender cómo organizar tu Dockerfile para que sirva tanto en desarrollo como en producción. Podemos resumir nuestros consejos así:

  • Intenta unificar el Dockerfile de desarrollo y producción; si no funciona, sepáralos.
  • No instales los node_modules de desarrollo en los builds de producción.
  • No dejes en la imagen final las dependencias de extensiones nativas necesarias para node-gyp y la instalación de node modules.
  • Usa docker-compose para orquestar tu entorno de desarrollo.
  • Para orquestar tus contenedores Docker en producción, tú eliges: docker-compose, Docker Swarm o Kubernetes.

Cuéntanos tu caso

Escríbenos