🐳 Docker Compose · Entorno de Desarrollo Local

LeadFlowAI — Stack completo en contenedores

Arquitectura multi-servicio lista para producción: frontend, API, DB, Redis, workers y email local. Un solo docker compose up para levantar todo.

6 servicios
2 redes aisladas
multi-stage Dockerfile
healthchecks + non-root
📐 Diagrama de red y servicios
🌐 public-net
🖥️
frontend
Next.js 14 · :3000
api
Express · :4000
📧
mailpit
SMTP :1025 · UI :8025
🔒 private-net
🐘
db (postgres:16-alpine)
127.0.0.1:5432 · healthcheck
🔴
redis (redis:7-alpine)
redis:6379 (interno)
⚙️
worker
Cola de leads async
1
docker-compose.yml

Stack completo con redes aisladas, healthchecks y dependencias encadenadas.

docker-compose.yml YAML
services:

  # ── Frontend (Next.js 14) ────────────────────────────
  frontend:
    build: { context: ./apps/frontend, target: dev }
    ports: ["3000:3000"]
    volumes:
      - ./apps/frontend:/app
      - /app/node_modules      # anon vol — protege deps del container
      - /app/.next             # protege build cache
    environment:
      - NEXT_PUBLIC_API_URL=http://api:4000
      - NODE_ENV=development
    depends_on:
      api: { condition: service_healthy }
    networks: [public-net]

  # ── API (Express) ────────────────────────────────────
  api:
    build: { context: ./apps/api, target: dev }
    ports: ["4000:4000"]
    volumes:
      - ./apps/api:/app
      - /app/node_modules
    env_file: [.env]
    environment:
      - DATABASE_URL=postgres://leadflow:secret@db:5432/leadflow_dev
      - REDIS_URL=redis://redis:6379/0
      - SMTP_HOST=mailpit
      - SMTP_PORT=1025
    depends_on:
      db:    { condition: service_healthy }
      redis: { condition: service_started }
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:4000/health || exit 1"]
      interval: 10s  timeout: 5s  retries: 3
    networks: [public-net, private-net]

  # ── PostgreSQL 16 ────────────────────────────────────
  db:
    image: postgres:16-alpine
    ports: ["127.0.0.1:5432:5432"]   # solo localhost, no red externa
    environment:
      POSTGRES_USER:     leadflow
      POSTGRES_PASSWORD: secret
      POSTGRES_DB:       leadflow_dev
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U leadflow"]
      interval: 5s  timeout: 3s  retries: 5
    networks: [private-net]

  # ── Redis 7 ──────────────────────────────────────────
  redis:
    image: redis:7-alpine
    volumes: [redisdata:/data]
    networks: [private-net]

  # ── Worker (procesamiento leads async) ───────────────
  worker:
    build: { context: ./apps/worker, target: dev }
    volumes: [./apps/worker:/app, /app/node_modules]
    env_file: [.env]
    environment:
      - REDIS_URL=redis://redis:6379/0
      - DATABASE_URL=postgres://leadflow:secret@db:5432/leadflow_dev
    depends_on:
      db:    { condition: service_healthy }
      redis: { condition: service_started }
    networks: [private-net]

  # ── Mailpit (email dev UI) ────────────────────────────
  mailpit:
    image: axllent/mailpit
    ports: ["8025:8025", "1025:1025"]
    networks: [public-net, private-net]

volumes:
  pgdata:
  redisdata:

networks:
  public-net:   # frontend + api + mailpit
  private-net:  # api + db + redis + worker
2
Override dev / prod

Archivos overlay para dev (auto-cargado) y producción (explícito).

docker-compose.override.yml YAML
# Auto-cargado en `docker compose up` — sólo en dev
services:
  api:
    environment:
      - LOG_LEVEL=debug
      - DEBUG=leadflow:*
    ports: ["9229:9229"]  # Node.js inspector
    command: node --inspect=0.0.0.0:9229 src/server.js

  worker:
    environment:
      - LOG_LEVEL=debug
3
Dockerfile multi-stage

Imagen compartida para API y Worker con stages: deps → dev → build → production.

apps/api/Dockerfile Dockerfile
# ── Stage: dependencies ──────────────────────────────
FROM node:22.12-alpine3.20 AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# ── Stage: dev (hot reload + inspector) ──────────────
FROM node:22.12-alpine3.20 AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 4000 9229
CMD ["npm", "run", "dev"]

# ── Stage: build ─────────────────────────────────────
FROM node:22.12-alpine3.20 AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production

# ── Stage: production (mínimo, non-root) ─────────────
FROM node:22.12-alpine3.20 AS production
WORKDIR /app

# Usuario non-root
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser

COPY --from=build --chown=appuser:appgroup /app/dist       ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./

ENV NODE_ENV=production
EXPOSE 4000
HEALTHCHECK --interval=30s --timeout=3s \
  CMD wget -qO- http://localhost:4000/health || exit 1
CMD ["node", "dist/server.js"]
4
Seguridad y secrets

Hardening del contenedor de producción y gestión correcta de credenciales.

docker-compose.prod.yml YAML
services:
  api:
    build:
      context: ./apps/api
      target: production
    restart: always
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs: [/tmp, /app/.cache]
    cap_drop: [ALL]
    deploy:
      resources:
        limits: { cpus: "1.0", memory: 512M }
    env_file: [.env.production]  # gitignored, cargado en CI/CD
.dockerignore Text
node_modules
.git
.env
.env.*            # NUNCA incluir en imagen
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
apps/*/node_modules
apps/*/.next
5
Anti-patrones vs buenas prácticas
✗ Evitar
⚠️Usar :latest — builds no reproducibles
⚠️Ejecutar como root en producción
⚠️Hardcodear secrets en docker-compose.yml
⚠️Datos sin volúmenes (efímeros)
⚠️Un solo contenedor con todo el stack
⚠️Exponer DB sin bind a 127.0.0.1
✓ Seguir
Tags fijas: node:22.12-alpine3.20
Usuario non-root con UID 1001
Secrets en .env (gitignored) o Docker Secrets
Volúmenes nombrados para persistencia
Un proceso por contenedor
Healthchecks en servicios críticos
6
Cheatsheet de comandos
🚀 Arrancar
docker compose upLevanta todo (dev)
docker compose up --buildReconstruye imágenes
docker compose up api dbSolo servicios concretos
docker compose -f dc.yml -f dc.prod.yml up -dModo producción
📋 Logs e inspección
docker compose logs -f apiLogs en vivo de la API
docker compose psEstado de servicios
docker statsCPU / RAM en tiempo real
docker compose topProcesos por contenedor
🔧 Debug
docker compose exec api shShell en el contenedor API
docker compose exec db psql -U leadflowConsola PostgreSQL
docker compose exec api nslookup dbVerifica DNS interno
docker network inspect leadflow_private-netInspecciona red privada
🧹 Limpieza
docker compose downPara y elimina containers
docker compose down -v+ elimina volúmenes (cuidado)
docker compose build --no-cache apiRebuild forzado
docker system pruneElimina recursos no usados