DevPath · Aprende a programar ESPTEN

Docker y CI/CD

Contenedores y Docker

El problema: "en mi máquina funciona"

Una aplicación depende de un entorno: una versión concreta de Node, ciertas variables, librerías del sistema... Si ese entorno difiere entre tu portátil y el servidor, aparecen errores difíciles de reproducir. Los contenedores resuelven esto empaquetando la aplicación junto con su entorno para que se ejecute igual en cualquier sitio.

Imagen vs contenedor

Es la distinción más importante del tema:

De una misma imagen puedes lanzar muchos contenedores. La imagen se construye una vez; los contenedores se crean, paran y destruyen continuamente.

El Dockerfile

Una imagen se define con un Dockerfile: una receta de instrucciones que se ejecutan de arriba abajo.

# Imagen base de la que partimos
FROM node:20-alpine

# Carpeta de trabajo dentro del contenedor
WORKDIR /app

# Copiamos primero los manifiestos (para aprovechar la caché, ver abajo)
COPY package.json package-lock.json ./
RUN npm ci

# Copiamos el resto del código fuente
COPY . .

# Comando que se ejecuta al arrancar el contenedor
CMD ["node", "server.js"]

Las instrucciones clave:

Capas y caché de build

Cada instrucción genera una capa. Docker cachea cada capa: si una instrucción y todo lo anterior no han cambiado, reutiliza la capa en lugar de re-ejecutarla. Por eso copiamos package.json y hacemos npm ci antes de copiar el resto del código: mientras no cambien las dependencias, esa capa (la lenta) se reutiliza aunque edites tu código fuente.

Regla práctica: pon lo que cambia poco arriba y lo que cambia mucho abajo.

Builds multi-stage

Para construir necesitas herramientas (compiladores, dev-dependencies) que no quieres en la imagen final. Un build multi-stage usa una etapa para construir y otra, limpia y pequeña, para ejecutar:

# Etapa 1: construir
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Etapa 2: imagen final, solo con lo necesario para ejecutar
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

La imagen final no contiene el código fuente ni las dev-dependencies: solo el resultado del build (dist) y las dependencias de producción. Resultado: imágenes más pequeñas, rápidas de desplegar y con menor superficie de ataque.

Varios servicios: docker-compose

Una app real combina varios contenedores (API, base de datos, caché...). docker-compose los describe y orquesta en un único fichero:

services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/app
    depends_on:
      - db
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: pass
    volumes:
      - db-data:/var/lib/postgresql/data

volumes:
  db-data:

Con docker compose up levantas toda la pila a la vez. Los servicios se ven entre sí por su nombre (db), y los volúmenes persisten datos aunque el contenedor se destruya.

Pon esto en práctica

DevPath es un curso práctico: aquí lees la teoría; en la app la pones en práctica con ejercicios que se ejecutan de verdad, sin conexión.

Empezar gratis en la app →
Integración continua (CI) →