Owners and admins can point a project at other systems: a signed JSON POST (HMAC-SHA256 of the body) when tickets, comments, releases or sprints change, filtered by event or family. Deliveries are retried a couple of times, the last one is shown, and a ping can be sent from the settings. Private addresses are refused unless WEBHOOKS_ALLOW_PRIVATE is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
14 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.ymles 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 → servicioweb, puerto80).webyapideclaranexposepara documentarlo.docker-compose.override.ymlpublica los puertos de la tabla. Compose lo carga solo cuando se ejecuta sin-f, es decir, en local; Dokploy usa-f docker-compose.ymly no lo lee.
Para probar en local exactamente lo de producción: docker compose -f docker-compose.yml up -d --build.
Nombres únicos en la red
Los servicios se llaman entre sí por alias únicos (task-stream-api, task-stream-postgres, task-stream-mongo,
task-stream-redis), no por el nombre del servicio. Al dar dominio a un servicio, Dokploy lo conecta a la red
compartida dokploy-network, donde otros proyectos también tienen un api, un postgres o un redis: con nombres
genéricos, el DNS de Docker devuelve también los suyos y nginx reparte las peticiones entre ambos (síntoma: la mitad
de las llamadas a /api/* responden 404 Cannot GET … y el login dice «no configurado»). Si añades un servicio,
dale su alias task-stream-* y úsalo en las URLs.
Todos tienen healthcheck; web espera a que api esté sana, y api a las tres bases de datos.
Imágenes
El Dockerfile tiene tres destinos que comparten la instalación de dependencias:
- Instalación:
npm cidel workspace.@pi/uiviene del Nexus privado; el token llega como build secret desde la variableNEXUS_TOKEN(secrets.nexus_tokenendocker-compose.yml), se escribe en unnpmrctemporal y se borra en el mismo paso, así que nunca queda en una capa. SinNEXUS_TOKENla build falla en ese paso. api:nx run api:prunegenera el bundle con unpackage.jsony un lockfile podados a partir deapps/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 aapps/api/package.json.mcp: el servidor MCP, podado igual que la API (nx run mcp:prune), sobre Node 24. Su servicio endocker-compose.ymlva en el perfilmcp: solo arranca con--profile mcp(en Dokploy,COMPOSE_PROFILES=mcp).web: build de producción de Angular servido por nginx (apps/web/nginx.conf): fallback de rutas aindex.html, caché larga para los bundles con hash y sin caché paraindex.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, PIREON_PASSWORD_CLIENT_ID, PIREON_PASSWORD_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 authorization_code: «Entrar con Pireon» y Swagger UI |
PIREON_PASSWORD_CLIENT_ID / _SECRET |
el anterior | Cliente OAuth password: login con email y contraseña |
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) |
SWAGGER_ENABLED |
true |
API REST documentada: Swagger UI en /api/docs, OpenAPI en /api/docs-json |
OPENAPI_FILE |
apps/api/openapi.json |
Dónde escribir el documento OpenAPI al arrancar (vacío = no escribir; así en Docker) |
ATTACHMENTS_DIR |
data/attachments |
Carpeta de los adjuntos de los tickets; en Docker, el volumen attachments-data |
SMTP_URL |
— | Servidor de correo para las copias de las notificaciones (smtps://user:pass@host); sin él no se envía nada |
MAIL_FROM |
TaskStream <no-reply@pireon.dev> |
Remitente de esos correos |
WEB_URL |
— | Dirección pública de la web, para los enlaces de los correos |
ATTACHMENTS_MAX_MB |
25 |
Tamaño máximo de un adjunto (nginx admite hasta 30 MB por petición) |
WEBHOOKS_ALLOW_PRIVATE |
false |
Deja que los webhooks salientes llamen a direcciones de esta máquina o de su red privada (apagado, un webhook podría alcanzar servicios internos) |
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. La excepción son los adjuntos: se guardan en disco (ATTACHMENTS_DIR), así que varias réplicas tienen que compartir ese volumen. - Relay: cada instancia publica eventos al stream;
FOR UPDATE SKIP LOCKEDevita duplicados. - Proyecciones: el
EventsBridgeprocesa 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_SYNCHRONIZEpor 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. Y del volumen de adjuntos (
attachments-data): los eventos dicen qué archivos hay, pero los bytes solo están ahí.
7.5 Reconstruir el read model
MongoDB solo contiene proyecciones, así que se puede regenerar desde el event store:
- Para la API (o desactiva el bridge en todas las instancias).
- Borra la base de lectura (
taskstream_read) salvo la colecciónusers, que no sale de los eventos sino de Pireon. - Marca los eventos como no publicados:
UPDATE events SET published_at = NULL; - 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.
Hazlo también una vez tras desplegar la configuración por proyecto y las estadísticas si quieres la historia completa
de los tickets anteriores: sin reconstruir, la línea temporal (issue_timeline) solo empieza con los cambios nuevos y
las estadísticas usan, para lo anterior, la creación y el estado actual de cada ticket. La categoría de los estados no
lo necesita: la API la rellena al arrancar en los tickets que no la tienen.