AboutOpinionesBlogLa Blockletter

Software Crafters® 2025 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal

Home » devops » Docker + NodeJS + Buenas Prácticas
Docker + NodeJS + Buenas Prácticas

Docker + NodeJS + Buenas Prácticas

*Dockerizar* un proyecto Nodejs es bastante sencillo, en este artículo veremos el paso a paso y comentaremos buenas prácticas a tener en cuenta.

Yodra López · Seguir5 min read ·

Tengo una newsletter en la que hablo de cómo diseñar mejor software (o escribir mejor código, que en realidad es lo mismo).

Al suscribirte, comparto contigo los libros que más me han hecho crecer como programador: los que todo dev debería leer al menos una vez.

Dockerizar un proyecto Nodejs es bastante sencillo, en este artículo veremos el paso a paso y comentaremos buenas prácticas a tener en cuenta. Docker se ha vuelto muy popular en los últimos años, y no es de extrañar, ha aportado grandes mejoras respecto a las clásicas máquinas virtuales.

Dockerizar nuestra aplicación Node.js

Para dockerizar nuestro proyecto necesitamos crear lo que denominamos Dockerfile. Este no es nada más que un archivo que define las instrucciones que debe de realizar Docker para construir el contenedor. Empecemos con uno simple para un proyecto Node.js y vamos añadiendo mejoras.

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

Este Dockerfile simplemente baja la imagen oficial de Node.js, copia nuestro proyecto, instala las dependencias y cuando Docker ejecute el contenedor, lanzará el comando

npm start
.

Este Dockerfile podríamos mejorarlo añadiendo algunas buenas prácticas:

FROM node:10-alpine
WORKDIR /usr/src/app

# Copiamos primero los ficheros necesarios para instalación de dependencias
COPY package*.json ./

# Instalamos dependencias
RUN npm ci --only=production

# Copiamos el código
COPY . .

# Definimos la variable PORT en el contenedor
ENV PORT=3000
EXPOSE $PORT

# Ejecutamos el comando en modo producción
ENV NODE_ENV=production
CMD ["npm", "start"]

Vamos a comentarlo paso a paso.

Usar imágenes alpine siempre que sea posible

Las imágenes base clasificadas con -alpine están basadas en la distribución Alpine Linux project, la cuál es mucho más ligera que la de otros sistemas operativos.

Optimizar el orden de las instrucciones (optimizar cache)

El orden de las instrucciones dentro del Dockerfile es importante por temas de eficiencia. Docker toma de referencia el checksum de una instrucción y sus dependencias (ficheros), si no ha cambiado, es capaz de utilizar la caché guardada. En el caso de un proyecto Node.js:

  1. No queremos reconstruir todo cada vez que nuestras fuentes cambian. Por eso es mejor instalar primero las dependencias (las cuáles no cambian tan habitualmente) y luego añadir el código fuente. Si el fichero package.json no cambia, la caché de esa capa se mantiene, agilizando la construcción de la imagen (evitar ejecutar npm install).
  2. Usamos npm ci en vez de npm install porque es una forma más limpia y rápida de instalar. Simplemente instala exactamente lo definido en el fichero package-lock.json en vez de resolver de nuevo las dependencias.

Usar la mínima cantidad de capas posible

Aunque docker soporta múltiples comandos

RUN
para instalar un servicio tras otro, es más óptimo juntar los comandos siempre que sea posible mediante el símbolo
&&
.

RUN curl -sL https://deb.nodesource.com/setup_10.x | bash - && apt-get update && apt-get install -y nodejs

Ejecutar el contenedor sin permisos de superusuario (root)

Ejecutar procesos como superusuario es considerado una vulnerabilidad en Docker. Crear un usuario dentro de la imagen y ejecutar la imagen con sus permisos es considerada buena práctica:

# Creamos usuario sin privilegios
RUN addgroup -g 1001 -S appuser && adduser -u 1001 -S appuser -G appuser

# Actualizar permisos
RUN chown -R appuser:appuser /usr/src/app

# Cambiar a usuario sin privilegios
USER appuser

No utilizar variables de entorno sensibles en tiempo de construcción

Usar variables de entorno con

ENV
durante la construcción de la imagen es útil para definir una configuración por defecto en el contenedor. Sin embargo, dado que esas variables quedan en las capas de la imagen, es importante no añadir secrets como tokens, contraseñas, etc. como variables de entorno.

Existen otras formas de añadir información sensible:

  1. Definir las variables en tiempo de ejecución:
    docker run -e "NODE_ENV=production" mi-imagen
  2. Usar ficheros .env con el comando docker run:
    docker run --env-file .env mi-imagen
  3. Usar docker-compose e incluir ficheros .env:
    env_file: .env
    Más información: Docker compose with env_file
  4. Usar docker swarm secrets para guardar información sensible. Docker swarm secrets

Mejorar el proceso de construcción con multi-stage

Mejorar la seguridad y construir una imagen donde no existan vulnerabilidades es un arte, y desde Docker 17.05 existe la posibilidad de usar multistage build para este propósito. Esto permite crear una imagen temporal de construcción donde compilas tu aplicación y luego moverla al contenedor final. Esta técnica permite separar lo que es el proceso de construcción de la imagen final de producción:

FROM node:10-alpine as build
WORKDIR /usr/src/app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM node:10-alpine
WORKDIR /usr/src/app

# Copiamos dependencias y el build de la fase anterior
COPY --from=build /usr/src/app/dist dist
COPY --from=build /usr/src/app/node_modules node_modules
COPY package*.json ./

ENV PORT=3000
EXPOSE $PORT

# Creamos usuario sin privilegios
RUN addgroup -g 1001 -S appuser && adduser -u 1001 -S appuser -G appuser

# Actualizar permisos
RUN chown -R appuser:appuser /usr/src/app

# Cambiar a usuario sin privilegios
USER appuser

ENV NODE_ENV=production
CMD ["npm", "start"]

De esta forma, nuestra imagen final solo tiene aquellos ficheros necesarios para ejecutar nuestra aplicación, dejando fuera todo el código fuente, ficheros de test, etc.

Usar .dockerignore

De la misma forma que gitignore, que ayuda a evitar subir ciertos ficheros o carpetas al repositorio, Docker cuenta con un fichero denominado .dockerignore que excluye ficheros y directorios a la hora de copiar el código en la instrucción COPY del Dockerfile.

Los ficheros excluidos típicamente son la carpeta node_modules, la carpeta .git, ficheros de configuración del IDE, archivos locales, variables de entorno, ficheros de log, etc.

.git
node_modules
.vscode
.DS_Store
coverage
.env*
*.log

Esto además ayuda a que la construcción de la imagen sea más rápida ya que tiene que copiar menos ficheros.

Tengo una newsletter en la que hablo de cómo diseñar mejor software (o escribir mejor código, que en realidad es lo mismo).

Al suscribirte, comparto contigo los libros que más me han hecho crecer como programador: los que todo dev debería leer al menos una vez.

Quizás te interese

Patrones de diseño con TypeScript en el mundo real: creacionales y estructurales

Patrones de diseño con TypeScript en el mundo real: creacionales y estructurales

TDD en React con TypeScript

TDD en React con TypeScript