Docker
El ERP se empaqueta en dos imágenes (API y frontend) publicadas en ghcr.io. En el servidor de
producción conviven con ~70 contenedores más del ecosistema (microservicios, verticales,
observabilidad, Vault) — esta página cubre las del monorepo zentto-web.
Reverse Proxy] --> API[zentto-api :4000
Node 20 + Express] NGINX --> FE[zentto-frontend
PM2: 17 apps Next.js :3000-3017] API --> PG[(PostgreSQL 16
host :5432 via gateway)] API --> VOL[/api_storage/] FE --> SHELL[shell :3000] FE --> APPS[16 micro-apps] subgraph Docker Network - zentto_zentto-net API FE end style NGINX fill:#10b981,color:#fff style API fill:#FF6584,color:#fff style FE fill:#6C63FF,color:#fff style PG fill:#374151,color:#fff
Dockerfile.api — API Node.js
Multi-stage con node:20-alpine. Además del build TypeScript, la imagen empaqueta
todo lo que el deploy necesita para migrar la BD:
# docker/Dockerfile.api (resumen)
FROM node:20-alpine AS builder
# npm ci con token (via secret npm_token — los @zentto/* son privados)
# npm run build → dist/
FROM node:20-alpine AS runner
# Instala goose v3.24.1 (binario, según TARGETARCH)
COPY dist/ ./dist
COPY web/contracts/openapi.yaml ./contracts/openapi.yaml
COPY web/api/migrations/postgres ./migrations/postgres
COPY web/api/sqlweb-pg ./sqlweb-pg
COPY scripts/bootstrap-db.sh .
EXPOSE 4000
CMD ["node", "dist/index.js"] Que las migraciones y sqlweb-pg/ viajen dentro de la imagen es lo que permite al
backoffice comparar la versión goose de cada BD dedicada contra el target y migrarla
(detalle).
Dockerfile.frontend — Next.js + PM2
Multi-stage; compila las micro-apps y las corre con pm2-runtime. Las variables
NEXT_PUBLIC_* se hornean en build-time vía ARGs:
# docker/Dockerfile.frontend (resumen)
FROM node:20-alpine AS builder
ARG NEXT_PUBLIC_API_BASE_URL # https://api.zentto.net en prod
ARG NEXT_PUBLIC_SHELL_URL
ARG NEXT_PUBLIC_AUTH_URL=https://auth.zentto.net # ¡se hornea en el bundle!
ARG NEXT_PUBLIC_TURNSTILE_SITE_KEY
ARG NEXT_PUBLIC_PADDLE_CLIENT_TOKEN
COPY web/modular-frontend/ .
RUN npm ci && npm run build:all
FROM node:20-alpine AS runner
RUN npm install -g pm2
COPY docker/pm2.config.cjs /app/pm2.config.cjs
EXPOSE 3000-3015 3017
CMD ["pm2-runtime", "/app/pm2.config.cjs"] NEXT_PUBLIC_AUTH_URL se hornea en build. Cambiarla en el env-file del
servidor no tiene efecto: hay que rebuild de la imagen con el ARG correcto. Aplica a todo
NEXT_PUBLIC_*.
PM2 — Ecosystem de procesos
docker/pm2.config.cjs define 17 procesos next start
(shell + 16 módulos). El mapa completo de puertos y basePaths está en
Micro-frontends. Cada proceso recibe el env runtime:
BACKEND_URL/API_URL, AUTH_SECRET,
AUTH_TRUST_HOST=true, AUTH_SERVICE_URL, más
NEXT_BASE_PATH y SHELL_URL=http://127.0.0.1:3000.
Compose
| Archivo | Uso |
|---|---|
docker-compose.yml | Local con build desde código |
docker-compose.dev.yml | Ambiente dev (imágenes :dev, BD zentto_dev) |
docker-compose.prod.yml | Producción — imágenes de ghcr.io, env-files del servidor |
# Producción
api: ghcr.io/zentto-erp/zentto-web/api:latest
frontend: ghcr.io/zentto-erp/zentto-web/frontend:latest
env_file: /opt/zentto/.env.api /opt/zentto/.env.frontend
networks:
zentto-net: { external: true, name: zentto_zentto-net }
volumes:
api_storage: { external: true, name: zentto_api_storage } docker restart NO relee el env-file. Tras editar
/opt/zentto/.env.*, recrear el contenedor
(docker compose up -d --force-recreate <svc>) o dejar que el deploy de CI lo haga.
Red Docker y PostgreSQL
PostgreSQL corre en el host. Los contenedores llegan por el gateway de su red Docker:
- PG_HOST:
172.18.0.1para la red principal (hay servicios en las redes 172.19/172.20 — cada uno usa su gateway). Nuncalocalhost. - pg_hba.conf:
host zentto_prod zentto_app 172.18.0.0/16 scram-sha-256(+ entradas por red/BD). - listen_addresses de PG incluye los gateways — un servicio en una red nueva requiere agregar su gateway y reiniciar PostgreSQL (reload no basta).
- El firewall es el de Hetzner Cloud en el borde (UFW está inactivo) — el tráfico Docker→host no pasa por él. Ver Servidor producción.
Puertos publicados en 127.0.0.1
Los contenedores publican sus puertos en 127.0.0.1:PUERTO (solo Nginx los alcanza),
no en 0.0.0.0. El mismo patrón aplica en el ambiente dev del
centro de datos dellxeon.
Si Nginx enruta a un puerto y responde 502, verificar que el PORT interno del
contenedor coincide con el puerto publicado — un mismatch da 502 permanente con deploy verde.