Por qué ganaron los contenedores
Antes de los contenedores, desplegar significaba reproducir un entorno: la versión correcta del runtime, las librerías del sistema adecuadas y la configuración precisa. El famoso “en mi máquina funciona” era un problema real, y los servidores tendían a divergir del entorno de desarrollo con el tiempo.
Docker solucionó esto empaquetando la aplicación y su entorno en una sola imagen. La imagen se ejecuta de forma idéntica en una laptop, en CI y en producción, porque lleva consigo todo lo que necesita. Esa consistencia es la razón por la cual los contenedores se convirtieron en el estándar para el despliegue, y por qué aprender Docker es una de las habilidades más valiosas para lanzar software.
Imágenes y contenedores
Una imagen es una plantilla de solo lectura creada a partir de un Dockerfile, compuesta por capas almacenadas en caché. Un contenedor es una instancia en ejecución de una imagen que posee su propia capa escribible, proceso y red. Construyes una vez y ejecutas muchas.
docker build -t my-app:1.0 .
docker run -p 3000:3000 my-app:1.0
docker ps
docker logs <container>
El flag -p mapea un puerto del host a un puerto del contenedor. Los contenedores son efímeros por diseño: puedes detenerlos, eliminarlos y recrearlos libremente porque la imagen es la fuente de verdad.
El Dockerfile
El Dockerfile es la receta de construcción. Cada instrucción crea una capa.
# Dockerfile
FROM node:22-alpine
WORKDIR /app
# install dependencies first for better caching
COPY package*.json ./
RUN npm ci --omit=dev
# then copy source
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
USER node
CMD ["node", "server.js"]
El orden es fundamental. Copiar los manifiestos de dependencias e instalarlas antes de copiar el código fuente significa que un cambio en el código solo invalida la capa de fuente (que es ligera), y no la de instalación (que es costosa). npm ci garantiza una instalación reproducible, y USER node se ejecuta como un usuario que no es root.
Caché de capas
Docker almacena en caché cada capa y la reutiliza cuando las entradas no han cambiado. Por esto, el orden anterior es deliberado: coloca los elementos que cambian rara vez en la parte superior y los que cambian con frecuencia en la parte inferior.
# cached unless package files change
COPY package*.json ./
RUN npm ci
# invalidated on every source change
COPY . .
Un archivo .dockerignore también es importante. Sin él, node_modules, .git y la salida de la compilación se copian en el contexto de construcción, lo que ralentiza el proceso y puede filtrar archivos dentro de la imagen.
node_modules
.git
dist
.env
Builds multi-etapa
Un build multi-etapa compila en una imagen y distribuye únicamente el resultado en otra.
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
La imagen final contiene solo los archivos estáticos compilados y un servidor web, no el runtime de Node, el código fuente ni las herramientas de construcción. El resultado es más pequeño, se descarga más rápido y tiene una superficie de ataque mucho menor. Para una aplicación de servidor, la etapa final sería una imagen slim de Node solo con las dependencias de producción.
Compose para aplicaciones multiservicio
Las aplicaciones reales suelen necesitar más de un proceso. Compose permite definirlos en conjunto.
# docker-compose.yml
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
volumes:
- data:/var/lib/postgresql/data
volumes:
data:
depends_on controla el orden de inicio, volumes persiste los datos entre reinicios de contenedores, y los servicios se comunican entre sí mediante su nombre en la red compartida. Compose es ideal para el desarrollo local y despliegues sencillos en un único host.
Registries y CI/CD
Las imágenes viajan a través de un registry como Docker Hub, GitHub Container Registry o un registry privado. Un pipeline típico consiste en:
- Instalar dependencias y ejecutar tests.
- Construir la imagen y etiquetarla con el SHA del commit.
- Hacer push al registry.
- Desplegar la nueva etiqueta en el entorno de destino.
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA
Debido a que la imagen es inmutable y está etiquetada por commit, los rollbacks consisten simplemente en desplegar una etiqueta anterior. Este es el núcleo de un proceso de despliegue fiable: construir una sola vez, promover el mismo artefacto a través de los entornos y nunca volver a construir para producción.
Prácticas de producción
- Health checks. Expón un endpoint que la plataforma pueda consultar para saber que el contenedor está listo.
- Usuario no root. Ejecuta el proceso con el usuario que tenga los privilegios mínimos necesarios.
- Sistema de archivos de solo lectura siempre que sea posible, con volúmenes de escritura explícitos.
- Graceful shutdown. Gestiona
SIGTERMpara finalizar las solicitudes en curso. - Límites de recursos. Configura la CPU y la memoria para que un contenedor no pueda agotar los recursos de los demás.
- Fija las versiones de la imagen base para garantizar builds reproducibles.
- Secretos en tiempo de ejecución, nunca integrados en la imagen.
- Imágenes base pequeñas, como Alpine o distroless, para reducir las vulnerabilidades.
Mejores prácticas
- Ordena las instrucciones del Dockerfile de la que menos cambia a la que más cambia.
- Utiliza multi-stage builds y despliega únicamente lo necesario.
- Añade un
.dockerignoreparanode_modules,.git,disty.env. - Ejecuta el proceso como un usuario que no sea root.
- Etiqueta las imágenes por el SHA del commit, no solo con
latest. - Construye la imagen una sola vez y promociónala a través de los entornos.
- Gestiona
SIGTERMpara un apagado controlado (graceful shutdown).
Errores comunes
- Copiar
node_modulesdentro de la imagen. - Ejecutar como root.
- Incluir secretos en la imagen o hacer commit de
.env. - Usar
latestcomo único tag y perder la reproducibilidad. - Reconstruir la imagen de forma distinta para cada entorno.
- Ignorar
.dockerignorey crear builds enormes y lentos.
Próximos pasos
Los contenedores hacen que el despliegue sea predecible, y las mismas habilidades se aplican ya sea que despliegues en una VM, Kubernetes o una plataforma de contenedores serverless. Refuerza los conceptos del runtime con la guía de Node.js, gestiona las dependencias con npm, sírvelo a través de HTTP y asegúralo con Web Security. Después, convierte una aplicación pequeña en un contenedor y despliégala de principio a fin.