Qué es Docker
Docker empaqueta una aplicación con todo lo que necesita — código, runtime, librerías, variables de entorno — en una imagen portable. Esa imagen corre igual en tu laptop, en staging y en producción. El problema de "en mi máquina funciona" desaparece.
# Instalar Docker Desktop (Mac / Windows) # https://docs.docker.com/desktop/ # Instalar en Ubuntu 24.04 curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # correr sin sudo newgrp docker # Verificar instalacion docker version # cliente y servidor docker info # info del daemon docker run hello-world # test basico — descarga imagen, corre, imprime mensaje # Conceptos clave en una linea docker pull nginx:alpine # descargar imagen de Docker Hub docker run -d -p 8080:80 nginx:alpine # correr contenedor en background docker ps # contenedores activos docker stop <id> # detener docker rm <id> # eliminar contenedor docker images # imagenes locales docker rmi nginx:alpine # eliminar imagen
docker compose up.Imágenes y Capas
Una imagen Docker es un stack de capas de solo lectura. Cada instrucción del Dockerfile añade una capa. Las capas se cachean y reutilizan entre builds — si no cambió, no se reconstruye.
# Buscar imágenes en Docker Hub docker search node docker search node --filter stars=1000 # Descargar imágenes docker pull node:20-alpine # Alpine = imagen minima (~40MB) docker pull node:20-slim # Slim = Debian minima (~200MB) docker pull postgres:16-alpine docker pull redis:7-alpine # Inspeccionar imagen docker images # listar imagenes locales docker image inspect node:20-alpine # metadata completa (JSON) docker history node:20-alpine # ver todas las capas y su tamano # Tagging — nombrar y versionar imagenes propias docker build -t erp-api:1.0.0 . docker tag erp-api:1.0.0 mi-registry/erp-api:1.0.0 docker tag erp-api:1.0.0 mi-registry/erp-api:latest # Exportar / importar imagen (sin registry) docker save erp-api:1.0.0 | gzip > erp-api.tar.gz docker load < erp-api.tar.gz # Limpiar imagenes no usadas docker image prune # solo imagenes dangling (sin tag) docker image prune -a # todas las no usadas docker system prune -af # limpieza total: imagenes + contenedores + redes
| Imagen base | Tamaño aprox | Ideal para |
|---|---|---|
| node:20-alpine | ~50MB | Producción — mínimo ataque surface |
| node:20-slim | ~220MB | Si necesitas herramientas Debian extras |
| node:20 | ~1.1GB | Solo dev — incluye build tools completos |
| postgres:16-alpine | ~240MB | PostgreSQL en todos los entornos |
| redis:7-alpine | ~30MB | Redis en todos los entornos |
| nginx:alpine | ~25MB | Reverse proxy / servir frontend |
Gestión de Contenedores
Un contenedor es una imagen en ejecución. docker run tiene decenas de flags para controlar puertos, variables de entorno, volúmenes, recursos y políticas de reinicio.
# Flags más usados docker run \ -d \ # detached (background) --name erp-api \ # nombre del contenedor -p 3000:3000 \ # puerto host:contenedor -e NODE_ENV=production \ # variable de entorno -e DB_HOST=postgres \ -v /data/logs:/app/logs \ # volumen: host:contenedor --network erp-network \ # conectar a red --restart unless-stopped \ # reiniciar si crashea --memory "512m" \ # limite de RAM --cpus "1.0" \ # limite de CPU erp-api:1.0.0 # imagen:tag # Ciclo de vida docker start erp-api # arrancar contenedor detenido docker stop erp-api # detener (SIGTERM, espera 10s, luego SIGKILL) docker kill erp-api # detener inmediato (SIGKILL) docker restart erp-api # stop + start docker pause erp-api # congelar proceso (SIGSTOP) docker unpause erp-api # Inspeccionar docker ps # contenedores activos docker ps -a # todos (incluye detenidos) docker logs erp-api # stdout/stderr docker logs erp-api --follow # tail -f docker logs erp-api --tail 50 # ultimas 50 lineas docker inspect erp-api # metadata completa JSON docker stats # CPU / RAM en tiempo real docker top erp-api # procesos dentro del contenedor # Ejecutar comandos dentro del contenedor docker exec -it erp-api sh # shell interactivo (Alpine usa sh) docker exec -it erp-api bash # bash (si esta disponible) docker exec erp-api node -v # comando no interactivo # Copiar archivos entre host y contenedor docker cp erp-api:/app/logs/error.log ./ docker cp ./config.json erp-api:/app/config.json
El Dockerfile
El Dockerfile define cómo se construye la imagen capa por capa. El orden importa: instrucciones que cambian poco van arriba (capas cacheadas), las que cambian frecuente van abajo. Un Dockerfile mal ordenado reconstruye todo en cada cambio de código.
# ── Instrucciones principales ────────────────────── FROM node:20-alpine # imagen base WORKDIR /app # directorio de trabajo (lo crea si no existe) # ARG — variable en tiempo de BUILD (no persiste en imagen final) ARG NODE_ENV=production # ENV — variable en tiempo de EJECUCION (persiste en la imagen) ENV NODE_ENV=$NODE_ENV ENV PORT=3000 # COPY — copiar archivos del contexto de build # Copiar solo package.json primero (para cachear npm install) COPY package*.json ./ RUN npm ci --omit=dev # instalar dependencias (capa cacheada) # Copiar el resto del codigo (esta capa se invalida con cada cambio) COPY . . # Compilar TypeScript si aplica RUN npm run build # Exponer puerto (documentacion — no publica el puerto) EXPOSE 3000 # USER — no correr como root en produccion USER node # HEALTHCHECK — Docker verifica que el contenedor esta sano HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget -qO- http://localhost:3000/health || exit 1 # CMD — comando por defecto al arrancar el contenedor CMD ["node", "dist/index.js"] # ENTRYPOINT — como CMD pero no se puede sobreescribir con docker run # ENTRYPOINT ["node", "dist/index.js"]
# Sin .dockerignore, COPY . . incluye node_modules, .git, etc. # Eso ralentiza el build y puede filtrar datos sensibles node_modules .git .gitignore *.log .env .env.* dist build coverage .nyc_output docker-compose*.yml *.md .DS_Store Thumbs.db
# Build básico docker build -t erp-api:1.0.0 . # Build con ARG (sobreescribir variables de build) docker build --build-arg NODE_ENV=development -t erp-api:dev . # Build desde Dockerfile con otro nombre docker build -f Dockerfile.prod -t erp-api:prod . # BuildKit — builder moderno (activado por defecto en Docker 23+) DOCKER_BUILDKIT=1 docker build -t erp-api:1.0.0 . # Ver cache de capas y cuales se reutilizaron docker build --progress=plain -t erp-api:1.0.0 . 2>&1 | grep -E "CACHED|RUN" # Forzar rebuild sin cache docker build --no-cache -t erp-api:1.0.0 .
package.json y corre npm ci ANTES de copiar el código fuente. Si cambias solo el código, Docker reutiliza la capa de node_modules (la más costosa). Mal orden = 2 min de CI. Buen orden = 15 segundos.Volúmenes y Persistencia
Los volúmenes persisten datos fuera del ciclo de vida del contenedor. Hay tres tipos: named volumes (gestionados por Docker), bind mounts (carpeta del host), y tmpfs (RAM, efímero). Para bases de datos siempre named volumes.
# ── Named volumes — gestionados por Docker ────────── docker volume create erp-pg-data # crear volumen docker volume ls # listar docker volume inspect erp-pg-data # ruta real en el host docker volume rm erp-pg-data # eliminar (solo si no está montado) docker volume prune # eliminar todos los no usados # Montar named volume en contenedor docker run -d \ --name postgres \ -v erp-pg-data:/var/lib/postgresql/data \ -e POSTGRES_PASSWORD=secreto \ postgres:16-alpine # ── Bind mount — carpeta del host ──────────────────── # Ideal para desarrollo (cambios de código sin rebuild) docker run -d \ --name erp-api-dev \ -v $(pwd)/src:/app/src \ -v $(pwd)/package.json:/app/package.json \ -p 3000:3000 \ erp-api:dev # ── tmpfs — en RAM, no persiste ────────────────────── # Util para archivos temporales de alta velocidad docker run --tmpfs /tmp:size=100m erp-api:1.0.0 # ── Backup de un volumen ────────────────────────────── # Crear tar del volumen montándolo en un contenedor auxiliar docker run --rm \ -v erp-pg-data:/data \ -v $(pwd):/backup \ alpine tar czf /backup/pg-backup.tar.gz /data # Restaurar desde tar docker run --rm \ -v erp-pg-data:/data \ -v $(pwd):/backup \ alpine tar xzf /backup/pg-backup.tar.gz -C /
| Tipo | Sintaxis | Caso de uso |
|---|---|---|
| Named volume | -v nombre:/ruta | Bases de datos, datos de producción |
| Bind mount | -v /host:/contenedor | Dev — hot reload de código fuente |
| tmpfs | --tmpfs /ruta | Archivos temp, tests, caches efímeros |
| Read-only | -v vol:/ruta:ro | Configuración y certificados seguros |
Redes Docker
Docker crea redes virtuales para que los contenedores se comuniquen entre sí. En una red bridge definida por el usuario, los contenedores se resuelven por nombre — la API llega a postgres con solo usar "postgres" como hostname.
# ── Tipos de red ────────────────────────────────────── # bridge — red privada virtual (default para contenedores) # host — el contenedor usa la red del host directamente # none — sin red (contenedor completamente aislado) # Crear red bridge personalizada docker network create erp-network docker network create \ --driver bridge \ --subnet 172.20.0.0/16 \ erp-network # Listar e inspeccionar docker network ls docker network inspect erp-network # ver contenedores conectados # Conectar contenedores a la red docker run -d --name postgres --network erp-network postgres:16-alpine docker run -d --name redis --network erp-network redis:7-alpine docker run -d --name erp-api --network erp-network \ -e DB_HOST=postgres \ -e REDIS_HOST=redis \ erp-api:1.0.0 # La API resuelve "postgres" y "redis" por DNS interno # Sin red personalizada, tendrias que usar IPs (cambian en cada restart) # Conectar/desconectar contenedor existente docker network connect erp-network erp-api docker network disconnect erp-network erp-api # Red host — util para rendimiento maximos (sin NAT) docker run --network host erp-api:1.0.0 # Test de conectividad dentro de la red docker exec erp-api ping postgres docker exec erp-api nslookup redis
bridge por defecto no permite resolución DNS por nombre — tienes que usar la IP (que cambia). Las redes definidas por el usuario sí incluyen DNS automático. En Compose, Docker crea automáticamente una red por proyecto — todos los servicios se ven por su nombre de servicio.Docker Compose
Compose define el stack completo en un archivo YAML: servicios, redes, volúmenes, variables de entorno y dependencias. Un solo docker compose up -d levanta todo el entorno. El estándar para desarrollo y staging.
services: # ── PostgreSQL ──────────────────────────────────── postgres: image: postgres:16-alpine container_name: erp-postgres restart: unless-stopped environment: POSTGRES_DB: erp_carniceria POSTGRES_USER: erp_user POSTGRES_PASSWORD: ${DB_PASSWORD} # desde .env volumes: - pg-data:/var/lib/postgresql/data - ./sql/init:/docker-entrypoint-initdb.d # scripts de init ports: - "5432:5432" # solo para dev / DBeaver healthcheck: test: ["CMD-SHELL", "pg_isready -U erp_user -d erp_carniceria"] interval: 10s timeout: 5s retries: 5 # ── Redis ───────────────────────────────────────── redis: image: redis:7-alpine container_name: erp-redis restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes volumes: - redis-data:/data healthcheck: test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 3s retries: 5 # ── API Node.js (Moleculer) ─────────────────────── api: build: context: ./api dockerfile: Dockerfile container_name: erp-api restart: unless-stopped depends_on: postgres: condition: service_healthy # esperar a que PG esté listo redis: condition: service_healthy environment: NODE_ENV: production DB_HOST: postgres # nombre del servicio = hostname DB_PORT: 5432 DB_NAME: erp_carniceria DB_USER: erp_user DB_PASSWORD: ${DB_PASSWORD} REDIS_HOST: redis REDIS_PASSWORD: ${REDIS_PASSWORD} JWT_SECRET: ${JWT_SECRET} ports: - "3000:3000" healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 # ── Nginx reverse proxy ─────────────────────────── nginx: image: nginx:alpine container_name: erp-nginx restart: unless-stopped depends_on: - api volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # read-only - ./frontend/dist:/usr/share/nginx/html:ro ports: - "80:80" - "443:443" # ── Volúmenes nombrados ─────────────────────────── volumes: pg-data: redis-data: # ── Red compartida ──────────────────────────────── networks: default: name: erp-network
# Arrancar todo el stack docker compose up -d # background docker compose up -d --build # rebuild de imagenes antes de arrancar docker compose up -d api # solo un servicio # Estado y logs docker compose ps # estado de todos los servicios docker compose logs -f # logs de todos en tiempo real docker compose logs -f api # logs de un servicio docker compose top # procesos de cada servicio # Detener docker compose stop # detener (conserva contenedores) docker compose down # detener y eliminar contenedores y redes docker compose down -v # down + eliminar volumenes (CUIDADO en prod) # Ejecutar comandos docker compose exec api sh # shell en el contenedor api docker compose exec postgres psql -U erp_user erp_carniceria docker compose run --rm api node scripts/migrate.js # contenedor temporal # Rebuild de un servicio especifico docker compose build api docker compose up -d --no-deps api # reload solo api sin reiniciar deps
Compose Avanzado
Perfiles para dev/prod, override files para configuración por entorno, extensiones YAML para no repetir config, y variables con .env. El mismo docker-compose.yml sirve en dev y en producción con overrides.
# docker-compose.override.yml se fusiona AUTOMÁTICAMENTE # en dev — agrega hot reload, volumes de código, puertos extras services: api: build: target: development # usar stage "development" del multi-stage volumes: - ./api/src:/app/src # bind mount para hot reload command: npm run dev # override del CMD de la imagen environment: NODE_ENV: development # Tool de administracion de BD solo en dev pgadmin: image: dpage/pgadmin4 profiles: [tools] # solo arranca con --profile tools ports: - "5050:80" environment: PGADMIN_DEFAULT_EMAIL: admin@erp.local PGADMIN_DEFAULT_PASSWORD: admin --- # docker-compose.prod.yml — configuracion de produccion # Usar con: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d services: api: deploy: replicas: 3 # 3 instancias de la API resources: limits: cpus: '1.0' memory: 512M restart_policy: condition: on-failure max_attempts: 3 logging: driver: json-file options: max-size: "20m" max-file: "5"
# .env — variables por entorno (nunca al repo) DB_PASSWORD=carniceria_super_secret REDIS_PASSWORD=redis_secret JWT_SECRET=jwt_super_secret_long_key --- # YAML anchors — definir una vez, reutilizar x-api-common: &api-common restart: unless-stopped networks: - erp-network logging: driver: json-file options: { max-size: "10m", max-file: "3" } services: api: <<: *api-common # hereda configuracion comun image: erp-api:latest ports: ["3000:3000"] worker: <<: *api-common # mismo logging, restart, network image: erp-api:latest command: node dist/worker.js # diferente comando # sin ports — worker no necesita exponer
Multi-stage Builds
Un Dockerfile multi-stage usa varios FROM. Los stages de build (con compiladores, devDependencies) se descartan. Solo el stage final llega a producción — imágenes 5-10x más pequeñas, sin herramientas de build, sin fuentes TypeScript.
# ── Stage 1: dependencias base ──────────────────── FROM node:20-alpine AS base WORKDIR /app RUN apk add --no-cache dumb-init # init process para Node.js en containers COPY package*.json ./ # ── Stage 2: desarrollo ─────────────────────────── FROM base AS development ENV NODE_ENV=development RUN npm install # todas las deps (incluye devDeps) COPY . . CMD ["npm", "run", "dev"] # ts-node-dev o nodemon # ── Stage 3: build ──────────────────────────────── FROM base AS builder RUN npm ci # install limpio (con devDeps para tsc) COPY . . RUN npm run build # tsc → dist/ RUN npm prune --omit=dev # eliminar devDependencies # ── Stage 4: produccion (imagen final) ─────────── FROM node:20-alpine AS production ENV NODE_ENV=production WORKDIR /app # Solo copiar lo necesario del stage builder COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY --from=base /usr/bin/dumb-init /usr/bin/dumb-init EXPOSE 3000 USER node # no root HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ CMD wget -qO- http://localhost:3000/health || exit 1 # dumb-init: maneja señales UNIX correctamente en containers ENTRYPOINT ["dumb-init", "--"] CMD ["node", "dist/index.js"]
# Build del stage de produccion (default) docker build -t erp-api:prod . # Build del stage de desarrollo (para docker-compose.override.yml) docker build --target development -t erp-api:dev . # Comparar tamaños docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" # erp-api dev ~580MB (node:20 + devDeps + sources TypeScript) # erp-api prod ~120MB (node:20-alpine + solo dist/ + prodDeps) # Ver por qué la imagen pesa lo que pesa docker history erp-api:prod # Dive — herramienta para explorar capas interactivamente docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock \ wagoodman/dive erp-api:prod
| Stage | Contiene | Tamaño típico | Destino |
|---|---|---|---|
| development | devDeps + fuentes TS | ~580MB | docker compose dev |
| builder | tsc + todo — temporal | ~650MB | descartado |
| production | dist/ + prodDeps solamente | ~120MB | Registry → prod |
Registry y CI/CD
El registry almacena imágenes. Docker Hub es el público. AWS ECR, GitHub Container Registry o un registry privado para producción. CI/CD construye, testea, publica y despliega automáticamente en cada push.
name: Build and Deploy ERP on: push: branches: [main] pull_request: branches: [main] env: REGISTRY: ghcr.io IMAGE: ghcr.io/${{ github.repository }}/erp-api jobs: build-and-push: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 # habilita BuildKit - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract metadata (tags, labels) id: meta uses: docker/metadata-action@v5 with: images: ${{ env.IMAGE }} tags: | type=sha,prefix=sha- # sha-abc1234 type=semver,pattern={{version}} # 1.2.3 si hay tag type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - name: Build and push uses: docker/build-push-action@v5 with: context: . target: production # stage de produccion push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha # cache de layers en GitHub Actions cache-to: type=gha,mode=max deploy: needs: build-and-push runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - name: Deploy to server via SSH uses: appleboy/ssh-action@v1 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} script: | cd /opt/erp docker compose pull api docker compose up -d --no-deps api docker image prune -f
Seguridad en Containers
Containers seguros por defecto: usuario no root, imagen base mínima, sin secretos en la imagen, read-only filesystem donde sea posible, y escaneo de vulnerabilidades en CI.
# ── 1. No correr como root ──────────────────────── # En el Dockerfile: RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser # o simplemente: USER node (ya existe en node:alpine) # En docker run: docker run --user 1001:1001 erp-api:prod # ── 2. Secretos: nunca en la imagen ────────────── # MAL — el secreto queda en la capa de la imagen # ENV DB_PASSWORD=secreto # BIEN — via variable de entorno en runtime docker run -e DB_PASSWORD=secreto erp-api:prod # O via Docker Secrets (Swarm) o Kubernetes Secrets # BuildKit secrets — disponible durante el build, no queda en imagen RUN --mount=type=secret,id=npm_token \ NPM_TOKEN=$(cat /run/secrets/npm_token) npm install docker build --secret id=npm_token,src=.npmrc -t erp-api . # ── 3. Read-only filesystem ─────────────────────── docker run --read-only \ --tmpfs /tmp \ # escritura solo en /tmp (RAM) --tmpfs /app/logs \ # logs en RAM (o usar volumen) erp-api:prod # ── 4. Capabilities — minimo privilegio ────────── docker run \ --cap-drop ALL \ # quitar TODAS las capabilities Linux --cap-add NET_BIND_SERVICE \ # re-agregar solo lo necesario --security-opt no-new-privileges \ erp-api:prod # ── 5. Escanear vulnerabilidades ───────────────── # Docker Scout (integrado) docker scout cves erp-api:prod docker scout recommendations erp-api:prod # Trivy (open source, muy completo) docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy image erp-api:prod # En GitHub Actions (Trivy) # - uses: aquasecurity/trivy-action@master # with: { image-ref: erp-api:prod, exit-code: '1', severity: 'CRITICAL' } # ── 6. Limite de recursos (evitar DoS) ─────────── docker run \ --memory 512m \ # max RAM --memory-swap 512m \ # sin swap (igual a memory = no swap) --cpus 1.0 \ # max 1 CPU --pids-limit 100 \ # max 100 procesos (prevenir fork bombs) erp-api:prod
--privileged le da al contenedor acceso completo al host — equivale a correr como root en el servidor. En el ERP nunca es necesario. Si algo pide --privileged, hay que cuestionarlo y buscar la alternativa con --cap-add específico.Monitoreo y Logs
Los contenedores escriben a stdout/stderr — Docker captura eso como logs. Para producción: log driver estructurado (JSON), rotación automática y agregación centralizada con Loki o CloudWatch.
# ── Logs Docker ─────────────────────────────────── docker logs erp-api # todos los logs docker logs erp-api -f # follow (tail -f) docker logs erp-api --since 1h # ultima hora docker logs erp-api --since "2024-01-15T10:00:00" docker logs erp-api 2>&1 | grep ERROR # filtrar errores # ── Log drivers ─────────────────────────────────── # json-file (default) — logs en disco con rotacion docker run \ --log-driver json-file \ --log-opt max-size=20m \ --log-opt max-file=5 \ erp-api:prod # awslogs — directo a CloudWatch (produccion en AWS) docker run \ --log-driver awslogs \ --log-opt awslogs-region=us-east-1 \ --log-opt awslogs-group=/erp/api \ --log-opt awslogs-stream=erp-api \ erp-api:prod # ── Metricas en tiempo real ─────────────────────── docker stats # todas los contenedores docker stats erp-api erp-postgres # especificos docker stats --no-stream --format \ "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}" # ── cAdvisor + Prometheus + Grafana (stack de monitoreo) ─ # docker-compose.monitoring.yml # cadvisor: expone metricas de contenedores en /metrics # prometheus: scrapea cadvisor y node-exporter # grafana: dashboards de CPU/RAM/red por contenedor # ── Node.js: escribir logs estructurados a stdout ── // import pino from 'pino'; // const logger = pino({ level: 'info' }); // escribe JSON a stdout // logger.info({ ventaId, total }, 'Venta registrada'); // Docker captura stdout → json-file → CloudWatch / Loki
El ERP en Docker
El stack completo del ERP para carnicerías en contenedores: PostgreSQL, Redis, API Moleculer (multi-instancia), Worker BullMQ y Nginx. Desde cero hasta producción con un solo repositorio.
services: postgres: image: postgres:16-alpine container_name: erp-pg restart: unless-stopped environment: POSTGRES_DB: erp_carniceria POSTGRES_USER: erp_api POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - pg-data:/var/lib/postgresql/data - ./migrations/sql:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD","pg_isready","-U","erp_api","-d","erp_carniceria"] interval: 10s; timeout: 5s; retries: 5 redis: image: redis:7-alpine container_name: erp-redis restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --maxmemory 512mb --maxmemory-policy allkeys-lru volumes: - redis-data:/data healthcheck: test: ["CMD","redis-cli","-a","${REDIS_PASSWORD}","ping"] interval: 10s; timeout: 3s; retries: 5 api: build: { context: ., target: production } image: erp-api:latest restart: unless-stopped depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } environment: NODE_ENV: production DB_HOST: postgres DB_PORT: 5432 DB_NAME: erp_carniceria DB_USER: erp_api DB_PASSWORD: ${DB_PASSWORD} REDIS_HOST: redis REDIS_PASSWORD: ${REDIS_PASSWORD} JWT_SECRET: ${JWT_SECRET} MOLECULER_NODE: api-${HOSTNAME} deploy: replicas: 3 # 3 instancias de la API resources: limits: { cpus: '1.0', memory: 512M } healthcheck: test: ["CMD","wget","-qO-","http://localhost:3000/health"] interval: 30s; timeout: 10s; retries: 3 worker: image: erp-api:latest # misma imagen, diferente comando restart: unless-stopped command: ["node", "dist/worker.js"] depends_on: postgres: { condition: service_healthy } redis: { condition: service_healthy } environment: <<: *api-env # hereda env del api (YAML anchor) MOLECULER_NODE: worker-${HOSTNAME} nginx: image: nginx:alpine container_name: erp-nginx restart: unless-stopped depends_on: api: { condition: service_healthy } volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./frontend/dist:/usr/share/nginx/html:ro - certbot-data:/etc/letsencrypt:ro ports: - "80:80" - "443:443" volumes: pg-data: redis-data: certbot-data:
docker compose up -d levanta PostgreSQL + Redis + API en segundos. En CI: multi-stage build produce una imagen de 120MB con solo el código compilado. En producción: 3 réplicas de la API detrás de Nginx, con healthchecks automáticos y deploy sin downtime. El mismo Dockerfile sirve para todo.