# 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_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