T-HUB/infra/occupymedia/docker-compose.app.yml
hacklab b47cac40ce fix(video): logo overlays must belong to the workspace; drop debug stderr
- produceVideo checks every logo overlay key against the workspace's
  media_assets, group_assets or branding logo before queueing; a
  well-formed content address alone no longer suffices.
- Remove MEDIA_DEBUG_STDERR=1 from the production compose: it wrote raw
  ffmpeg stderr to an unrotated file, bypassing redaction.

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

193 lines
7.1 KiB
YAML

# occupymedia.cc — APP CT (occ-app, CT 150, tailscale 100.78.162.2)
# Control room: postgres/redis/temporal + api/scheduler/web/media-worker + the tunnel.
# Ports bind 0.0.0.0 in the CT: public access is ONLY via the Cloudflare tunnel
# (ingress: app/api → 100.78.162.2). LAN/tailnet can reach them directly.
name: occ-app
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:-occupymedia}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?}
POSTGRES_DB: ${POSTGRES_DB:-occupymedia}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-occupymedia}"]
interval: 5s
retries: 10
restart: unless-stopped
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes: [redisdata:/data]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
retries: 10
restart: unless-stopped
temporal:
image: temporalio/auto-setup:1.24
depends_on:
postgres: { condition: service_healthy }
environment:
DB: postgres12
DB_PORT: 5432
POSTGRES_USER: ${POSTGRES_USER:-occupymedia}
POSTGRES_PWD: ${POSTGRES_PASSWORD:?}
POSTGRES_SEEDS: postgres
restart: unless-stopped
api:
build:
context: ../..
dockerfile: infra/occupymedia/Dockerfile
args: { APP: api }
environment: &appenv
PORT: "3000"
# Runtime pools use the non-owner role; owner access is offline only.
DATABASE_URL: ${DATABASE_URL:?Set the sp_app runtime URL}
REDIS_HOST: redis
REDIS_URL: redis://redis:6379
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
TEMPORAL_HOST: temporal
TEMPORAL_PORT: "7233"
TEMPORAL_ADDRESS: "temporal:7233"
S3_ENDPOINT: https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com
S3_REGION: auto
S3_ACCESS_KEY: ${R2_ACCESS_KEY:-unset}
S3_SECRET_KEY: ${R2_SECRET_KEY:-unset}
S3_BUCKET: occupymedia-media-encrypted
S3_KEK: ${S3_KEK:?}
SUPERPOSTER_KEK: ${SUPERPOSTER_KEK:?}
SESSION_SECRET: ${SESSION_SECRET:?}
CORS_ORIGINS: https://app.occupymedia.cc,https://files.occupymedia.cc
PUBLIC_ORIGIN: https://app.occupymedia.cc
WEB_ORIGIN: https://app.occupymedia.cc
API_PUBLIC_URL: https://api.occupymedia.cc
# OAuth routing: providers redirect the browser to CONNECTIONS_CALLBACK_URL
# after consent; the API then lands it on APP_URL. Both must be public —
# unset, both default to localhost and the flow breaks silently.
APP_URL: https://app.occupymedia.cc
CONNECTIONS_CALLBACK_URL: https://api.occupymedia.cc/connections/callback
PEERTUBE_INSTANCE_URL: https://tube.occupymedia.cc
PEERTUBE_AUTHORIZE_PATH: /oauth/authorize
PEERTUBE_TOKEN_PATH: /api/v1/users/token
# Group files: oCIS basic-auth WebDAV (values live in the compose .env)
FILES_OCID_WEBDAV_URL: ${FILES_OCID_WEBDAV_URL:?}
FILES_OCID_USER: ${FILES_OCID_USER:?}
FILES_OCID_PASSWORD: ${FILES_OCID_PASSWORD:?}
FILES_PUBLIC_BASE: ${FILES_PUBLIC_BASE:-https://files.occupymedia.cc}
ARCHIVE_PROVISIONER_HOMESERVER_URL: ${ARCHIVE_PROVISIONER_HOMESERVER_URL:-}
ARCHIVE_PROVISIONER_USER: ${ARCHIVE_PROVISIONER_USER:-}
ARCHIVE_PROVISIONER_PASSWORD: ${ARCHIVE_PROVISIONER_PASSWORD:-}
# In-app Chat sign-in: appservice tokens, see appservice.hub in tuwunel.toml.
# Unset = the Chat view falls back to Element's own sign-in screen.
MATRIX_SSO_JWT_SECRET: ${MATRIX_SSO_JWT_SECRET:-}
MATRIX_SSO_AS_TOKEN: ${MATRIX_SSO_AS_TOKEN:-}
MATRIX_SSO_HS_TOKEN: ${MATRIX_SSO_HS_TOKEN:-}
MATRIX_SSO_HOMESERVER_URL: ${MATRIX_SSO_HOMESERVER_URL:-https://chat.occupymedia.cc}
MATRIX_SSO_LOCALPART_PREFIX: ${MATRIX_SSO_LOCALPART_PREFIX:-}
# In-app Drive: oCIS web origin + auth mode (interactive | oidc).
FILES_DRIVE_URL: ${FILES_DRIVE_URL:-https://files.occupymedia.cc}
FILES_DRIVE_AUTH: ${FILES_DRIVE_AUTH:-interactive}
# Publishing workflows and the scheduler worker MUST share one queue,
# else approved publishes sit unconsumed.
TEMPORAL_TASK_QUEUE: ${TEMPORAL_TASK_QUEUE:-tmbh-publishing}
# Hub-as-OIDC-issuer signing keys (see docs/design/2026-10-13-files-oidc-sso.md).
OIDC_KEY_DIR: /var/lib/tmbh/oidc
volumes:
- oidckeys:/var/lib/tmbh/oidc
ports: ["3000:3000"]
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
restart: unless-stopped
scheduler:
build:
context: ../..
dockerfile: infra/occupymedia/Dockerfile
args: { APP: scheduler }
environment:
<<: *appenv
PORT: "3010"
SCHEDULER_DATABASE_URL: ${SCHEDULER_DATABASE_URL:?Set the sp_app runtime URL}
SCHEDULER_HEALTH_PORT: "8081"
TEMPORAL_TASK_QUEUE: ${TEMPORAL_TASK_QUEUE:-tmbh-publishing}
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8081/readyz').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 3
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
temporal: { condition: service_started }
restart: unless-stopped
web:
build:
context: ../..
dockerfile: infra/occupymedia/Dockerfile
args:
APP: web
NEXT_PUBLIC_API_URL: https://api.occupymedia.cc
environment:
<<: *appenv
PORT: "3001"
ports: ["3001:3001"]
depends_on: [api]
restart: unless-stopped
media-worker:
build:
context: ../..
dockerfile: infra/media-worker.Dockerfile
environment:
<<: *appenv
PORT: "3100"
MEDIA_WORKER_ID: worker-1
MEDIA_FENCE_PATH: /var/lib/media-worker/fence.json
POSTGRES_HOST: postgres
REDIS_HOST: redis
# cross-CT: peertube RTMP on occ-tube via tailscale
PEERTUBE_PUSH_URL: rtmp://100.92.231.98:1935/live
volumes: [mediaworkerstate:/var/lib/media-worker]
depends_on:
redis: { condition: service_healthy }
restart: unless-stopped
# Produce worker (editor v2): claims video_processing_jobs kind compose|mix,
# renders assembled/overlaid/voiced cuts, registers derived library assets.
# Deliberately its own process: a remote-lane box needs only Postgres +
# object storage + ffmpeg (see apps/media-worker/src/produce.ts header).
produce-worker:
build:
context: ../..
dockerfile: infra/media-worker.Dockerfile
command: ["pnpm", "--filter", "@tactical-media-broadcast-hub/media-worker", "exec", "tsx", "src/produce.ts"]
environment:
<<: *appenv
MEDIA_WORKER_LANE: local
MEDIA_SCRATCH_DIR: /var/lib/media-worker/produce
volumes: [mediaworkerstate:/var/lib/media-worker]
depends_on:
postgres: { condition: service_healthy }
restart: unless-stopped
cloudflared:
image: cloudflare/cloudflared:latest
command: ["tunnel", "--no-autoupdate", "run", "--token", "${CLOUDFLARE_TUNNEL_TOKEN:?}"]
restart: unless-stopped
volumes:
pgdata:
redisdata:
mediaworkerstate:
oidckeys: