Dokploy builds failed with "failed to stat /root/.npmrc": the compose build
secret pointed at ${HOME}/.npmrc, which only exists on a developer machine.
The token now comes from the NEXUS_TOKEN environment variable (Dokploy's
Environment tab or .env), is written to a temporary npmrc and removed in the
same RUN, so it never lands in an image layer.
Also removes a temporary UI dev-server spec that was committed by mistake,
and documents the variable (.env.example, docs 06/07, Word/PDF export).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.7 KiB
7.7 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 | 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 |
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 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.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 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 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.
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.