Files
task-stream/docker-compose.yml
Josep Piguillem 233c46e9fa feat: push notifications on each device (Web Push)
- What reaches the bell also reaches the devices where the person
  turned «Avisos en este dispositivo» on, with TaskStream closed: the
  Notifier sends each new notification encrypted (aes128gcm) and signed
  with VAPID, and forgets subscriptions the push service dropped
  (404/410). A failure never blocks the event stream.
- The API keeps one subscription per device (push_subscriptions) and
  exposes the VAPID public key in the notification settings; GraphQL
  subscribePush/unsubscribePush, REST /me/push-subscriptions. Without
  VAPID_PUBLIC_KEY and VAPID_PRIVATE_KEY push is off.
- The web registers /push-sw.js, a service worker that only shows the
  notification and opens its ticket on a tap (no caching, served
  without cache by nginx), and a manifest that makes it installable,
  which iPhones need for push.
- The bell's toggles show their text next to them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 08:38:07 +02:00

155 lines
5.6 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:-}
# Push notifications (optional): `npx web-push generate-vapid-keys`.
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-}
# 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