Files
task-stream/docs/07-despliegue.md
Josep Piguillem b2c9be72c3 chore(docker): publish ports only in development
docker-compose.yml is the production definition (deployed by Dokploy) and no
longer publishes any port: postgres, mongo, redis and the API are reachable
only on the compose network, and the web goes out through the platform's
proxy (domain → service web, port 80). The ports moved to
docker-compose.override.yml, which Compose loads automatically locally and
Dokploy (-f docker-compose.yml) does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:16:50 +02:00

8.4 KiB

7. Despliegue

7.1 Docker

docker compose up -d --build                 # todo: web en http://localhost:8080
docker compose up -d postgres mongo redis    # solo infraestructura
docker compose logs -f api
docker compose down                          # parar (-v borra los datos)
Servicio Imagen Puerto en local Función
web task-stream-web (nginx) 8080 SPA y proxy de /api y /graphql (incluido el WebSocket) hacia la API
api task-stream-api (Node 22) 3000 API NestJS
postgres postgres:17-alpine 5432 Event store y lookups
mongo mongo:8 27017 Read model
redis redis:7-alpine (AOF) 6379 Stream de eventos, pub/sub y caché de sesiones

Producción y desarrollo

  • docker-compose.yml es la definición de producción (la que despliega Dokploy) y no publica puertos: los servicios solo se ven entre sí por la red de Compose. Hacia fuera solo sale la web, por el proxy de la plataforma (en Dokploy: dominio → servicio web, puerto 80). web y api declaran expose para documentarlo.
  • docker-compose.override.yml publica los puertos de la tabla. Compose lo carga solo cuando se ejecuta sin -f, es decir, en local; Dokploy usa -f docker-compose.yml y no lo lee.

Para probar en local exactamente lo de producción: docker compose -f docker-compose.yml up -d --build.

Todos tienen healthcheck; web espera a que api esté sana, y api a las tres bases de datos.

Imágenes

El Dockerfile tiene dos destinos que comparten la instalación de dependencias:

  • Instalación: npm ci del workspace. @pi/ui viene del Nexus privado; el token llega como build secret desde la variable NEXUS_TOKEN (secrets.nexus_token en docker-compose.yml), se escribe en un npmrc temporal y se borra en el mismo paso, así que nunca queda en una capa. Sin NEXUS_TOKEN la build falla en ese paso.
  • api: nx run api:prune genera el bundle con un package.json y un lockfile podados a partir de apps/api/package.json. La imagen final solo instala esas dependencias (sin Angular ni paquetes privados). Si la API empieza a usar un paquete nuevo en tiempo de ejecución, hay que añadirlo a apps/api/package.json.
  • web: build de producción de Angular servido por nginx (apps/web/nginx.conf): fallback de rutas a index.html, caché larga para los bundles con hash y sin caché para index.html.

Construir

NEXUS_TOKEN tiene que estar en el entorno de quien construye, o en el .env junto al docker-compose.yml:

# Local: reutiliza el token de tu ~/.npmrc
export NEXUS_TOKEN=$(grep '^//nexus.pireon.dev/repository/npm-releases/:_authToken=' ~/.npmrc | cut -d= -f2-)
docker compose up -d --build

En Dokploy (despliegue con Docker Compose): pestaña Environment del servicio → NEXUS_TOKEN=<token>, junto a PIREON_CLIENT_ID, PIREON_CLIENT_SECRET y PIREON_WEBHOOK_SECRET. Dokploy los escribe en el .env del proyecto, de donde los lee Compose.

7.2 Variables de entorno de la API

Variable Por defecto Descripción
PORT 3000 Puerto HTTP
NODE_ENV development En production no se devuelven stacktraces
POSTGRES_URL — (obligatoria) Event store y lookups
POSTGRES_SYNCHRONIZE false Crea las tablas desde las entidades. Cómodo en desarrollo; en producción, migraciones
MONGO_URL — (obligatoria) Read model
REDIS_URL — (obligatoria) Stream, pub/sub y caché
EVENT_STREAM_KEY task-stream:events Clave del Redis Stream
EVENT_STREAM_MAX_LENGTH 1000000 Longitud aproximada que se conserva en el stream
EVENTS_BRIDGE_ENABLED true Si esta instancia proyecta eventos hacia Mongo (ver escalado)
PIREON_BASE_URL https://oauth.pireon.dev Servidor de identidad
PIREON_CLIENT_ID / PIREON_CLIENT_SECRET — Cliente OAuth para el login desde la web
PIREON_SCOPES users:read Scopes que se piden al iniciar sesión
PIREON_PASSWORD_GRANT true Permite el login con email y contraseña
PIREON_WEBHOOK_SECRET — Activa y firma los webhooks de Pireon
PIREON_TIMEOUT_MS 5000 Timeout de las llamadas a Pireon
AUTH_SESSION_TTL_SECONDS 60 Cuánto se confía en un token validado antes de volver a preguntar a Pireon
GRAPHQL_PLAYGROUND true GraphiQL e introspección
GRAPHQL_SCHEMA_FILE apps/api/schema.gql Dónde escribir el SDL (vacío = no escribir; así está en Docker)

La web no tiene configuración de entorno: llama a la API en su mismo origen (/api, /graphql).

7.3 Escalado

  • API: es stateless (sesiones en Redis, subscriptions por Redis pub/sub), así que admite varias réplicas detrás del balanceador. El WebSocket necesita que el balanceador permita Upgrade.
  • Relay: cada instancia publica eventos al stream; FOR UPDATE SKIP LOCKED evita duplicados.
  • Proyecciones: el EventsBridge procesa los eventos en orden. Lo más sencillo es dejarlo activo en una sola instancia (EVENTS_BRIDGE_ENABLED=true) y desactivarlo en las demás. Varias instancias en el mismo consumer group se reparten los eventos y pierden el orden entre ellos.

7.4 Antes de producción

  • Sustituir POSTGRES_SYNCHRONIZE por migraciones de TypeORM.
  • Registrar el cliente OAuth y las redirect URIs de producción en Pireon.
  • Servir por HTTPS (la web usa wss:// automáticamente cuando la página va por HTTPS).
  • Revisar GRAPHQL_PLAYGROUND (introspección) según el entorno.
  • Copias de seguridad de PostgreSQL: es la fuente de verdad.

7.5 Reconstruir el read model

MongoDB solo contiene proyecciones, así que se puede regenerar desde el event store:

  1. Para la API (o desactiva el bridge en todas las instancias).
  2. Borra la base de lectura (taskstream_read) salvo la colección users, que no sale de los eventos sino de Pireon.
  3. Marca los eventos como no publicados: UPDATE events SET published_at = NULL;
  4. Arranca la API: el relay vuelve a enviar todos los eventos, en orden, y las proyecciones reconstruyen las vistas. Las notificaciones en tiempo real se reenvían también, sin efecto si no hay clientes conectados.