📐 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 reproduciblesEjecutar como
root en producciónHardcodear secrets en
docker-compose.ymlDatos 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.20Usuario non-root con UID 1001
Secrets en
.env (gitignored) o Docker SecretsVolú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