New app apps/mcp (NestJS + @nestjs-mcp/server 2): Streamable HTTP at POST /mcp, stateless. - 24 tools: projects (mine, public, details with settings, board, stats, analytics, activity), tickets (my work, search, details with comments and history, create, update, move, assign, release, comment, link), releases (list, upcoming, details with progress and burnup, create, publish) and users (whoami, search). Admin operations stay in the web. - No credentials of its own: each call's Authorization header (a Pireon token) is forwarded to the API; without it only public projects are read. - Typed REST client generated from apps/api/openapi.json (openapi-fetch + openapi-typescript via `nx run mcp:openapi-types`); API refusals become readable tool errors; parameters validated with zod. - Tests: the real MCP against a simulated API (token forwarding, errors, validation, writes). - Docker target `mcp` (Node 24) and an optional compose service under the `mcp` profile; zod declared as a root dependency. - Docs: chapter 9 «Servidor MCP», references in 01/06/07 and the index; Word/PDF regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
10 KiB
Markdown
130 lines
10 KiB
Markdown
# 7. Despliegue
|
|
|
|
## 7.1 Docker
|
|
|
|
```sh
|
|
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`.
|
|
|
|
### 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 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`.
|
|
- **`mcp`**: el [servidor MCP](09-mcp.md), podado igual que la API (`nx run mcp:prune`), sobre Node 24. Su servicio
|
|
en `docker-compose.yml` va en el perfil `mcp`: 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 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`:
|
|
|
|
```sh
|
|
# 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) |
|
|
|
|
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.
|
|
|
|
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.
|