// 01 — QUÉ ES DOCKER

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.

📦
Imagen = receta
Snapshot inmutable del sistema. Capas reutilizables. Se construye una vez, corre en cualquier lugar.
🏃
Contenedor = proceso
Instancia en ejecución de una imagen. Aislado, efímero, reproducible. Arranca en milisegundos.
📋
Dockerfile = código
Instrucciones para construir la imagen. Vive en el repo. Se revisa, versiona y automatiza.
🎼
Compose = orquestación
Define y arranca múltiples contenedores con un archivo YAML. El entorno completo en un comando.
instalacion + verificacionCLI
# 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
🐳
VM vs ContainerUna VM virtualiza hardware completo con un OS guest (varios GB, minutos para arrancar). Un contenedor comparte el kernel del host y solo aísla el proceso — arranca en milisegundos, pesa MB. Para el ERP usamos contenedores para Node.js, PostgreSQL, Redis y Nginx — todo el stack en un docker compose up.

// 02 — IMÁGENES Y CAPAS

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.

comandos de imágenesCLI
# 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 baseTamaño aproxIdeal para
node:20-alpine~50MBProducción — mínimo ataque surface
node:20-slim~220MBSi necesitas herramientas Debian extras
node:20~1.1GBSolo dev — incluye build tools completos
postgres:16-alpine~240MBPostgreSQL en todos los entornos
redis:7-alpine~30MBRedis en todos los entornos
nginx:alpine~25MBReverse proxy / servir frontend

// 03 — CONTENEDORES

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.

docker run — flags esencialesCLI
# 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
⚠️
Los contenedores son efímerosTodo lo que escribas dentro de un contenedor (sin volumen) se pierde al borrarlo. Las bases de datos, uploads y logs deben montarse en volúmenes persistentes. El código de la app va en la imagen — no en volúmenes.

// 04 — DOCKERFILE

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.

Dockerfile — instrucciones esencialesDOCKERFILE
# ── 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"]
.dockerignore — excluir archivos del contexto de buildCONFIG
# 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
docker build — construir la imagenCLI
# 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 .
💡
El orden del Dockerfile = velocidad del CI/CDSiempre copia 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.

// 05 — VOLÚMENES Y DATOS

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.

volúmenes — comandos y tiposCLI
# ── 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 /
TipoSintaxisCaso de uso
Named volume-v nombre:/rutaBases de datos, datos de producción
Bind mount-v /host:/contenedorDev — hot reload de código fuente
tmpfs--tmpfs /rutaArchivos temp, tests, caches efímeros
Read-only-v vol:/ruta:roConfiguración y certificados seguros

// 06 — REDES DOCKER

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.

redes docker — comandos y tiposCLI
# ── 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
🌐
Red bridge default vs definida por usuarioLa red 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.

// 07 — DOCKER COMPOSE

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.

docker-compose.yml — stack del ERPCOMPOSE
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
comandos docker compose esencialesCLI
# 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

// 08 — COMPOSE AVANZADO

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 — configuración devCOMPOSE
# 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 y YAML anchors — evitar repeticionCONFIG
# .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
Demo — simulador Docker Compose: validar config y dependencias▶ LIVE
// Simular docker compose up con healthchecks const services = { postgres: { image: 'postgres:16-alpine', healthCmd: 'pg_isready', ready: false, deps: [] }, redis: { image: 'redis:7-alpine', healthCmd: 'redis-cli ping', ready: false, deps: [] }, api: { image: 'erp-api:latest', healthCmd: 'wget /health', ready: false, deps: ['postgres','redis'] }, nginx: { image: 'nginx:alpine', healthCmd: 'curl /nginx', ready: false, deps: ['api'] }, }; async function healthCheck(name, svc) { await new Promise(r => setTimeout(r, Math.random() * 300 + 100)); svc.ready = true; console.log(`✅ ${name} healthy (${svc.healthCmd})`); } async function composeUp(services) { console.log('🐳 docker compose up -d\n'); const started = new Set(); async function startService(name) { if (started.has(name)) return; const svc = services[name]; // esperar dependencias for (const dep of svc.deps) { if (!started.has(dep)) { console.log(` ⏳ ${name} esperando: ${dep}`); await startService(dep); } } console.log(` 🚀 Iniciando ${name} (${svc.image})`); await healthCheck(name, svc); started.add(name); } await Promise.all(Object.keys(services).map(startService)); console.log(`\n🎉 Stack listo — ${started.size} servicios activos`); } await composeUp(services);
// outputHaz clic en Ejecutar...

// 09 — MULTI-STAGE BUILDS

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.

Dockerfile — multi-stage para API Node.js TypeScriptDOCKERFILE
# ── 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 de stage específico + comparativa de tamañosCLI
# 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
StageContieneTamaño típicoDestino
developmentdevDeps + fuentes TS~580MBdocker compose dev
buildertsc + todo — temporal~650MBdescartado
productiondist/ + prodDeps solamente~120MBRegistry → prod

// 10 — REGISTRY Y CI/CD

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.

.github/workflows/docker.yml — GitHub Actions CI/CDCI/CD
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

// 11 — SEGURIDAD

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.

seguridad — mejores prácticasSEC
# ── 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
🚨
docker run --privileged es casi siempre un error--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.

// 12 — MONITOREO Y LOGS

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.

logging y monitoreo de contenedoresOPS
# ── 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

// 13 — ERP EN DOCKER

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.

postgres:16-alpine redis:7-alpine node:20-alpine (api) node:20-alpine (worker) nginx:alpine multi-stage build healthchecks secrets via env
docker-compose.yml — stack completo del ERPREAL
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:
Demo — simulador de deploy: build → push → deploy▶ LIVE
// Simular pipeline CI/CD: build → test → push → deploy async function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } async function runStep(name, ms, ok = true) { process.stdout && process.stdout.write; console.log(` ⏳ ${name}...`); await sleep(ms); if (!ok) throw new Error(`${name} falló`); console.log(` ✅ ${name} completado`); } async function cicdPipeline(sha, branch) { console.log(`🚀 Pipeline iniciado`); console.log(` Branch: ${branch} | SHA: ${sha.slice(0,8)}`); console.log(''); // Stage 1: Build console.log('📦 Stage 1: Build'); await runStep('docker build --target production', 1200); await runStep('Imagen: erp-api:sha-' + sha.slice(0,8), 100); // Stage 2: Test console.log('\n🧪 Stage 2: Tests'); await runStep('docker run --rm erp-api:dev npm test', 800); await runStep('trivy image scan (0 CRITICAL)', 400); // Stage 3: Push console.log('\n📤 Stage 3: Push Registry'); await runStep('docker push erp-api:sha-' + sha.slice(0,8), 600); if (branch === 'main') { await runStep('docker push erp-api:latest', 300); } // Stage 4: Deploy (solo main) if (branch === 'main') { console.log('\n🌐 Stage 4: Deploy producción'); await runStep('SSH → docker compose pull api', 400); await runStep('docker compose up -d --no-deps api', 500); await runStep('healthcheck: GET /health → 200 OK', 300); await runStep('docker image prune -f', 100); console.log('\n🎉 Deploy completado — 3 replicas activas'); } else { console.log(`\n⏩ PR build — no deploy (solo en main)`); } } await cicdPipeline('a1b2c3d4e5f6', 'main');
// outputHaz clic en Ejecutar...
🐳
Docker completa el stack del ERPEn dev: 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.