- 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>
193 lines
7.1 KiB
YAML
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:
|