FullStackJS Camp
Módulo 9·practica·8h
Objetivos de aprendizaje
  • Escribir un Dockerfile multi-stage optimizado para Next.js
  • Configurar Docker Compose para orquestar frontend, API y base de datos
  • Aplicar buenas prácticas de seguridad (usuario no-root, .dockerignore)
  • Entender los conceptos fundamentales de contenedorización y Kubernetes

Docker y Contenedorización

Docker resuelve el problema clásico de "funciona en mi máquina": empaqueta la aplicación con todas sus dependencias en una imagen que corre de forma idéntica en desarrollo, CI y producción.

Conceptos fundamentales

ConceptoDescripción
ImagenPlantilla inmutable que define el sistema de archivos y el proceso que se ejecuta
ContenedorInstancia en ejecución de una imagen (como un proceso aislado)
DockerfileScript de instrucciones para construir una imagen
RegistryRepositorio de imágenes (Docker Hub, GitHub Container Registry, GCP Artifact Registry)
Docker ComposeHerramienta para definir y levantar múltiples contenedores con un solo archivo YAML
VolumeMecanismo para persistir datos más allá del ciclo de vida de un contenedor
NetworkRed virtual que permite la comunicación entre contenedores

Dockerfile multi-stage para Next.js

La estrategia multi-stage usa varias etapas de build para que la imagen final solo contenga lo necesario (sin herramientas de build, sin código fuente).

dockerfile
# Dockerfile (raíz del proyecto Next.js)

# ──── Etapa 1: Dependencias de producción ────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app

# Copiar solo manifiestos de paquetes (aprovecha caché de Docker)
COPY package.json package-lock.json ./
RUN npm ci --only=production

# ──── Etapa 2: Builder ───────────────────────────────────────────────
FROM node:20-alpine AS builder
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

# Variables necesarias en tiempo de build
ARG NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL

RUN npm run build

# ──── Etapa 3: Runner (imagen final) ─────────────────────────────────
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Seguridad: crear usuario sin privilegios
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copiar solo los artefactos necesarios del builder
COPY --from=builder /app/public ./public

# Next.js standalone: incluye solo los módulos que usa la app
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# Cambiar al usuario no-root antes de iniciar
USER nextjs

EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]

Para que el modo standalone funcione, añade al next.config.ts:

ts
// next.config.ts
const nextConfig = {
  output: 'standalone', // ← genera una carpeta .next/standalone
};

export default nextConfig;

Dockerfile para la API Express

dockerfile
# api/Dockerfile

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

# Usuario no-root
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 apiuser

# Copiar dependencias y código fuente
COPY --from=deps /app/node_modules ./node_modules
COPY --chown=apiuser:nodejs . .

USER apiuser

EXPOSE 4000
CMD ["node", "src/index.js"]

.dockerignore

dockerignore
# .dockerignore — evita copiar archivos innecesarios al contexto de build
node_modules
.next
.env
.env.local
.env.*.local
*.log
npm-debug.log*
.git
.gitignore
README.md
Dockerfile
docker-compose*.yml
coverage
.nyc_output

Docker Compose — orquestación del proyecto completo

yaml
# docker-compose.yml
services:
  # ── Base de datos ─────────────────────────────────
  db:
    image: postgres:16-alpine
    container_name: factordash-db
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - factordash-net
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5

  # ── API Express ───────────────────────────────────
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: factordash-api
    environment:
      NODE_ENV: production
      PORT: 4000
      DATABASE_URL: postgres://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
      JWT_SECRET: ${JWT_SECRET}
      JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET}
    depends_on:
      db:
        condition: service_healthy
    networks:
      - factordash-net
    ports:
      - "4000:4000"

  # ── Frontend Next.js ──────────────────────────────
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
      args:
        NEXT_PUBLIC_API_URL: http://api:4000
    container_name: factordash-frontend
    environment:
      NODE_ENV: production
    depends_on:
      - api
    networks:
      - factordash-net
    ports:
      - "3000:3000"

volumes:
  postgres_data:

networks:
  factordash-net:
    driver: bridge
env
# .env (en la raíz del proyecto — NO commitear)
DB_USER=factordash
DB_PASSWORD=supersecret
DB_NAME=factordash_db
JWT_SECRET=mi-jwt-secret-largo-y-aleatorio
JWT_REFRESH_SECRET=mi-refresh-secret-largo

Comandos esenciales

bash
# Construir e iniciar todos los servicios
docker compose up --build

# Iniciar en background
docker compose up -d --build

# Ver logs de un servicio específico
docker compose logs -f api

# Detener y eliminar contenedores (los volúmenes se conservan)
docker compose down

# Detener y eliminar contenedores + volúmenes (borra la BD)
docker compose down -v

# Ejecutar un comando dentro de un contenedor en ejecución
docker compose exec api sh

# Ver imágenes construidas
docker images

# Ver contenedores en ejecución
docker ps

# Limpiar imágenes, contenedores y redes sin uso
docker system prune -a

Introducción a Kubernetes

Kubernetes (K8s) es el orquestador de contenedores estándar de la industria. Cuando Docker Compose gestiona contenedores en una máquina, K8s los gestiona en un clúster de máquinas.

Concepto K8sEquivalente Docker Compose
PodContenedor (puede agrupar varios)
Deploymentbuild + política de réplicas
Serviceports + load balancer interno
IngressReverse proxy (Nginx/Traefik)
ConfigMapVariables de entorno no sensibles
SecretVariables de entorno sensibles
NamespaceSeparación lógica (dev/staging/prod)
yaml
# Ejemplo conceptual: Deployment de la API en K8s
apiVersion: apps/v1
kind: Deployment
metadata:
  name: factordash-api
spec:
  replicas: 3         # 3 instancias del contenedor
  selector:
    matchLabels:
      app: factordash-api
  template:
    metadata:
      labels:
        app: factordash-api
    spec:
      containers:
        - name: api
          image: gcr.io/mi-proyecto/factordash-api:latest
          ports:
            - containerPort: 4000
          env:
            - name: NODE_ENV
              value: "production"
          resources:
            limits:
              cpu: "500m"
              memory: "256Mi"

Ejercicios propuestos

  1. Construye la imagen del frontend con docker build -t factordash-frontend . y verifica que el servidor arranca con docker run -p 3000:3000 factordash-frontend.
  2. Levanta el stack completo (frontend + api + db) con docker compose up --build y verifica que el login funciona end-to-end.
  3. Inspecciona el tamaño de la imagen antes y después de usar el modo standalone de Next.js con docker images.