Files
task-stream/docker-compose.yml
Josep Piguillem 5ae1d05d30 fix(api): accept Pireon webhooks from the password client too
Pireon signs each webhook with the secret of the client that registered
it, and the API has two clients (the web app's and the password
grant's). Only PIREON_WEBHOOK_SECRET was accepted, so a token revoked
for someone signed in with a password went unheard. A delivery now
verifies with PIREON_WEBHOOK_SECRET or PIREON_PASSWORD_WEBHOOK_SECRET;
the same event arriving from both clients changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 13:29:27 +02:00

151 lines
5.4 KiB
YAML

# TaskStream: web + API + infrastructure — production definition (Dokploy deploys this file).
# No ports are published here: services only talk over the compose network, and the web is
# exposed through the platform's proxy (Dokploy domain → service `web`, port 80).
#
# Services talk to each other by unique aliases (task-stream-*), never by the bare service
# name: on a shared network (Dokploy's dokploy-network) other projects also have an `api`,
# `postgres` or `redis`, and Docker DNS would hand out theirs too.
#
# Locally, `docker compose` also loads docker-compose.override.yml, which publishes the ports:
# docker compose up -d --build → everything: web on http://localhost:8080
# docker compose up -d postgres mongo redis → only infrastructure (then `npx nx serve api` / `npx nx serve web`)
#
# Building needs access to Pireon's Nexus (@pi/ui): set NEXUS_TOKEN in the environment or in .env
# (Dokploy: Environment tab). It reaches the build as a secret and never lands in an image layer.
services:
web:
build:
context: .
target: web
secrets: [nexus_token]
image: task-stream-web
expose: ['80']
networks: [default]
depends_on:
api: { condition: service_healthy }
restart: unless-stopped
api:
build:
context: .
target: api
secrets: [nexus_token]
image: task-stream-api
expose: ['3000']
networks:
default:
aliases: [task-stream-api]
environment:
POSTGRES_URL: postgres://taskstream:taskstream@task-stream-postgres:5432/taskstream
# No migrations yet: TypeORM creates the event store and lookup tables.
POSTGRES_SYNCHRONIZE: 'true'
MONGO_URL: mongodb://task-stream-mongo:27017/taskstream_read
REDIS_URL: redis://task-stream-redis:6379
EVENT_STREAM_KEY: task-stream:events
EVENTS_BRIDGE_ENABLED: 'true'
PIREON_BASE_URL: https://oauth.pireon.dev
PIREON_WEBHOOK_SECRET: ${PIREON_WEBHOOK_SECRET:-}
PIREON_PASSWORD_WEBHOOK_SECRET: ${PIREON_PASSWORD_WEBHOOK_SECRET:-}
PIREON_CLIENT_ID: ${PIREON_CLIENT_ID:-}
PIREON_CLIENT_SECRET: ${PIREON_CLIENT_SECRET:-}
# Pireon clients allow one grant type: the one above is authorization_code,
# this one password (email + password sign-in). Optional.
PIREON_PASSWORD_CLIENT_ID: ${PIREON_PASSWORD_CLIENT_ID:-}
PIREON_PASSWORD_CLIENT_SECRET: ${PIREON_PASSWORD_CLIENT_SECRET:-}
GRAPHQL_PLAYGROUND: 'true'
# The SDL / OpenAPI files are only useful in development (codegen); the
# document is still served at /api/docs-json.
GRAPHQL_SCHEMA_FILE: ''
OPENAPI_FILE: ''
ATTACHMENTS_DIR: /data/attachments
ATTACHMENTS_MAX_MB: ${ATTACHMENTS_MAX_MB:-25}
# E-mail copies of notifications (optional).
SMTP_URL: ${SMTP_URL:-}
MAIL_FROM: ${MAIL_FROM:-}
WEB_URL: ${WEB_URL:-}
# Outgoing webhooks to private addresses (off: they could reach internal services).
WEBHOOKS_ALLOW_PRIVATE: ${WEBHOOKS_ALLOW_PRIVATE:-false}
# Due-date reminders, checked every hour.
REMINDERS_ENABLED: ${REMINDERS_ENABLED:-true}
REMINDERS_TIMEZONE: ${REMINDERS_TIMEZONE:-Europe/Madrid}
volumes:
# Files attached to tickets: lose this volume and the tickets point at nothing.
- attachments-data:/data/attachments
depends_on:
postgres: { condition: service_healthy }
mongo: { condition: service_healthy }
redis: { condition: service_healthy }
restart: unless-stopped
# MCP server for AI clients (Claude, Cursor…): Streamable HTTP at /mcp. It consumes the API
# with each caller's own Pireon token (Authorization header), so it holds no credentials.
# Optional: it only starts with the `mcp` profile (`docker compose --profile mcp up -d`; in
# Dokploy, COMPOSE_PROFILES=mcp) and needs a domain → service `mcp`, port 3010.
mcp:
profiles: [mcp]
build:
context: .
target: mcp
secrets: [nexus_token]
image: task-stream-mcp
expose: ['3010']
networks:
default:
aliases: [task-stream-mcp]
environment:
TASKSTREAM_API_URL: http://task-stream-api:3000
# Domains it answers on (protection against DNS rebinding): the one Dokploy gives it.
MCP_ALLOWED_HOSTS: ${MCP_ALLOWED_HOSTS:-mcp.taskstream.pireon.dev,localhost}
MCP_LOG_LEVEL: ${MCP_LOG_LEVEL:-warn}
depends_on:
api: { condition: service_healthy }
restart: unless-stopped
postgres:
image: postgres:17-alpine
networks:
default:
aliases: [task-stream-postgres]
environment:
POSTGRES_USER: taskstream
POSTGRES_PASSWORD: taskstream
POSTGRES_DB: taskstream
volumes: [postgres-data:/var/lib/postgresql/data]
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U taskstream']
interval: 5s
retries: 10
mongo:
image: mongo:8
networks:
default:
aliases: [task-stream-mongo]
volumes: [mongo-data:/data/db]
healthcheck:
test: ['CMD', 'mongosh', '--quiet', '--eval', 'db.adminCommand("ping")']
interval: 5s
retries: 10
redis:
image: redis:7-alpine
networks:
default:
aliases: [task-stream-redis]
command: ['redis-server', '--appendonly', 'yes']
volumes: [redis-data:/data]
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 5s
retries: 10
volumes:
attachments-data:
postgres-data:
mongo-data:
redis-data:
secrets:
nexus_token:
environment: NEXUS_TOKEN