df18128617
Tails OCIS's JSON-Lines audit log on a shared Docker volume and forwards
mutations into AuditService. Final piece of Phase 2 — the /audit page now
unifies platform-api, authentik, and ocis events on one timeline.
services/platform-api/src/ingest/ocis.ingest.ts:
- 5s polling loop (fs.watch is unreliable across Docker bind mounts on
macOS). Stat → detect inode change or truncation → resume from byte
position OR start over.
- Cursor in IngestCursor stores lastEventId = "<inode>:<bytePosition>".
Restarts resume cleanly; on overlap the (source, externalId) unique
index dedups silently.
- Lines collected first, then processed sequentially after the read
stream closes. Earlier draft fired recordOne() from inside the
readline 'line' callback which would have resolved the stream
before all writes finished — same class of race we hit in the
Authentik worker, fixed before commit.
- Tenant inference: spaceName (set during provisioning to the slug)
first, then User.authentikSubjectId → tenantIds → Tenant.slug.
- Mutations only: OCIS_ALLOWLIST in action-map.ts whitelists 24 event
types (User/Group/Space/Share/Link/File mutations). FileDownloaded,
UserSignedIn, and the rest of the high-volume read traffic gets
skipped — keeps the timeline scannable.
services/platform-api/src/ingest/action-map.ts:
- mapOcisAction() + OCIS_ALLOWLIST. Returns null for non-whitelisted
types so the worker filters early.
infrastructure/docker-compose/docker-compose.yml:
- New named volume `ocis_audit_log` mounted writeable on the ocis
container and read-only on platform-api.
- OCIS env: OCIS_ADD_RUN_SERVICES=audit (the audit microservice is
NOT in the default `ocis server` set — opt in explicitly),
AUDIT_LOG_FILE_PATH=/var/log/ocis/audit.log, AUDIT_LOG_FORMAT=json.
- platform-api env: OCIS_AUDIT_LOG_PATH points at the same file.
Verified end-to-end with synthetic events written to the audit log:
- Worker tailed 5 events across initial read + incremental append
(5 → bytes 0:1276, then 1 → bytes 1276:1519).
- FileDownloaded correctly filtered by the allowlist (4 mutations
landed in Mongo, not 5).
- Tenant inference: events with executingUser.id resolved to
`dezky` via User → tenantIds → Tenant.slug.
- Operator /audit shows all three sources (89 events: 79 authentik
+ 5 platform-api + 5 ocis) in one unified timeline.
Known unknown — same shape as the Stalwart commit: I couldn't fully
confirm the OCIS v7 audit microservice emits events with just
OCIS_ADD_RUN_SERVICES=audit + the AUDIT_LOG_FILE_PATH env. The audit
service starts but the file stays empty until OCIS internals start
publishing events to NATS (which may need additional service-side
config). The ingest worker is correct regardless — when OCIS starts
writing real events, they'll flow into /audit. This is a follow-up
in the OCIS-side configuration, not in our ingest code.
499 lines
23 KiB
YAML
499 lines
23 KiB
YAML
# Dezky — Local Development Stack
|
|
#
|
|
# Start: docker compose up -d
|
|
# Logs: docker compose logs -f [service]
|
|
# Stop: docker compose down
|
|
# Reset: docker compose down -v (WARNING: deletes all data)
|
|
#
|
|
# Prerequisites:
|
|
# 1. mkcert root CA installed (mkcert -install)
|
|
# 2. Wildcard cert generated in ./certs/dezky.local.pem
|
|
# 3. /etc/hosts entries added (run scripts/setup-hosts.sh)
|
|
# 4. .env file created from .env.example
|
|
|
|
name: dezky
|
|
|
|
networks:
|
|
dezky:
|
|
name: dezky
|
|
driver: bridge
|
|
|
|
volumes:
|
|
postgres_data:
|
|
mongo_data:
|
|
redis_data:
|
|
authentik_media:
|
|
authentik_certs:
|
|
authentik_templates:
|
|
stalwart_data:
|
|
ocis_config:
|
|
ocis_data:
|
|
ocis_audit_log:
|
|
portal_node_modules:
|
|
platform_api_node_modules:
|
|
operator_node_modules:
|
|
|
|
services:
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Traefik — Reverse proxy with TLS termination via mkcert
|
|
# ─────────────────────────────────────────────────────────────────
|
|
traefik:
|
|
image: traefik:v3.7
|
|
container_name: dezky-traefik
|
|
restart: unless-stopped
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
- "8443:8080" # Dashboard
|
|
volumes:
|
|
- ${DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock:ro
|
|
- ./configs/traefik/traefik.yml:/etc/traefik/traefik.yml:ro
|
|
- ./configs/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
|
|
- ./certs:/certs:ro
|
|
networks:
|
|
dezky:
|
|
aliases:
|
|
- traefik.dezky.local
|
|
- auth.dezky.local
|
|
- app.dezky.local
|
|
- operator.dezky.local
|
|
- api.dezky.local
|
|
- files.dezky.local
|
|
- mail.dezky.local
|
|
- office.dezky.local
|
|
- collaboration.dezky.local
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.dashboard.rule=Host(`traefik.dezky.local`)
|
|
- traefik.http.routers.dashboard.service=api@internal
|
|
- traefik.http.routers.dashboard.tls=true
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# PostgreSQL — Shared RDBMS (Authentik, OCIS)
|
|
# ─────────────────────────────────────────────────────────────────
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
container_name: dezky-postgres
|
|
restart: unless-stopped
|
|
environment:
|
|
POSTGRES_PASSWORD: ${POSTGRES_ROOT_PASSWORD}
|
|
POSTGRES_DB: postgres
|
|
AUTHENTIK_DB_PASSWORD: ${AUTHENTIK_DB_PASSWORD}
|
|
OCIS_DB_PASSWORD: ${OCIS_DB_PASSWORD}
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
- ./configs/postgres/init.sh:/docker-entrypoint-initdb.d/init.sh:ro
|
|
networks: [dezky]
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# MongoDB — Portal application data
|
|
# ─────────────────────────────────────────────────────────────────
|
|
mongo:
|
|
image: mongo:7
|
|
container_name: dezky-mongo
|
|
restart: unless-stopped
|
|
environment:
|
|
MONGO_INITDB_ROOT_USERNAME: root
|
|
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD}
|
|
MONGO_INITDB_DATABASE: dezky
|
|
volumes:
|
|
- mongo_data:/data/db
|
|
networks: [dezky]
|
|
healthcheck:
|
|
test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Redis — Cache + session store
|
|
# ─────────────────────────────────────────────────────────────────
|
|
redis:
|
|
image: redis:7-alpine
|
|
container_name: dezky-redis
|
|
restart: unless-stopped
|
|
command: redis-server --save 60 1 --loglevel warning --requirepass ${REDIS_PASSWORD}
|
|
volumes:
|
|
- redis_data:/data
|
|
networks: [dezky]
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Authentik — Identity provider (OIDC/SAML SSO)
|
|
# ─────────────────────────────────────────────────────────────────
|
|
authentik-server:
|
|
image: ghcr.io/goauthentik/server:2025.10
|
|
container_name: dezky-authentik
|
|
restart: unless-stopped
|
|
command: server
|
|
environment:
|
|
AUTHENTIK_REDIS__HOST: redis
|
|
AUTHENTIK_REDIS__PASSWORD: ${REDIS_PASSWORD}
|
|
AUTHENTIK_POSTGRESQL__HOST: postgres
|
|
AUTHENTIK_POSTGRESQL__USER: authentik
|
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
|
|
AUTHENTIK_POSTGRESQL__NAME: authentik
|
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
|
|
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
|
|
AUTHENTIK_DISABLE_UPDATE_CHECK: "true"
|
|
AUTHENTIK_BOOTSTRAP_EMAIL: admin@dezky.local
|
|
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}
|
|
AUTHENTIK_BOOTSTRAP_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN}
|
|
volumes:
|
|
- authentik_media:/media
|
|
- authentik_certs:/certs
|
|
- authentik_templates:/templates
|
|
- ./configs/authentik/blueprints:/blueprints/custom:ro
|
|
networks: [dezky]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.authentik.rule=Host(`auth.dezky.local`)
|
|
- traefik.http.routers.authentik.tls=true
|
|
- traefik.http.services.authentik.loadbalancer.server.port=9000
|
|
|
|
authentik-worker:
|
|
image: ghcr.io/goauthentik/server:2025.10
|
|
container_name: dezky-authentik-worker
|
|
restart: unless-stopped
|
|
command: worker
|
|
environment:
|
|
AUTHENTIK_REDIS__HOST: redis
|
|
AUTHENTIK_REDIS__PASSWORD: ${REDIS_PASSWORD}
|
|
AUTHENTIK_POSTGRESQL__HOST: postgres
|
|
AUTHENTIK_POSTGRESQL__USER: authentik
|
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASSWORD}
|
|
AUTHENTIK_POSTGRESQL__NAME: authentik
|
|
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
|
|
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
|
|
volumes:
|
|
- authentik_media:/media
|
|
- authentik_certs:/certs
|
|
- authentik_templates:/templates
|
|
networks: [dezky]
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Stalwart Mail — Mail server (SMTP/IMAP/JMAP/CalDAV/CardDAV/ActiveSync)
|
|
# ─────────────────────────────────────────────────────────────────
|
|
stalwart:
|
|
image: stalwartlabs/stalwart:v0.16
|
|
container_name: dezky-stalwart
|
|
restart: unless-stopped
|
|
ports:
|
|
- "25:25" # SMTP
|
|
- "465:465" # SMTPS
|
|
- "587:587" # Submission
|
|
- "143:143" # IMAP
|
|
- "993:993" # IMAPS
|
|
- "4190:4190" # ManageSieve
|
|
environment:
|
|
STALWART_FQDN: mail.dezky.local
|
|
# Pin the recovery admin so it survives restarts. Without this, Stalwart
|
|
# generates a one-time-shown password at first boot and discards it after
|
|
# initial setup.
|
|
STALWART_RECOVERY_ADMIN: admin:${STALWART_ADMIN_PASSWORD}
|
|
# Shared HMAC secret for the audit webhook POSTed to platform-api.
|
|
# config.toml references this via %{env:STALWART_WEBHOOK_SECRET}%.
|
|
STALWART_WEBHOOK_SECRET: ${STALWART_WEBHOOK_SECRET}
|
|
volumes:
|
|
- stalwart_data:/opt/stalwart
|
|
- ./configs/stalwart/config.toml:/opt/stalwart/etc/config.toml:ro
|
|
networks: [dezky]
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.stalwart.rule=Host(`mail.dezky.local`)
|
|
- traefik.http.routers.stalwart.tls=true
|
|
- traefik.http.services.stalwart.loadbalancer.server.port=8080
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# OCIS — File storage with S3-compatible backend
|
|
# ─────────────────────────────────────────────────────────────────
|
|
ocis:
|
|
image: owncloud/ocis:7.0
|
|
container_name: dezky-ocis
|
|
restart: unless-stopped
|
|
entrypoint: /bin/sh
|
|
command: ["-c", "ocis init --insecure true || true && ocis server"]
|
|
environment:
|
|
OCIS_URL: https://files.dezky.local
|
|
OCIS_LOG_LEVEL: warn
|
|
OCIS_INSECURE: "true" # dev only — self-signed certs
|
|
PROXY_HTTP_ADDR: 0.0.0.0:9200
|
|
PROXY_TLS: "false" # Traefik terminates TLS; OCIS speaks plain HTTP internally
|
|
OCIS_OIDC_ISSUER: https://auth.dezky.local/application/o/ocis/
|
|
WEB_OIDC_CLIENT_ID: ocis-web
|
|
PROXY_AUTOPROVISION_ACCOUNTS: "true"
|
|
PROXY_USER_OIDC_CLAIM: preferred_username
|
|
PROXY_USER_CS3_CLAIM: username
|
|
OCIS_ADMIN_USER_ID: ""
|
|
IDM_CREATE_DEMO_USERS: "false"
|
|
IDM_ADMIN_PASSWORD: ${OCIS_ADMIN_PASSWORD}
|
|
STORAGE_USERS_DRIVER: ocis # Local filesystem in dev
|
|
STORAGE_SYSTEM_DRIVER: ocis
|
|
OCIS_CONFIG_DIR: /etc/ocis
|
|
OCIS_BASE_DATA_PATH: /var/lib/ocis
|
|
# Extend CSP so the OCIS web SPA can reach Authentik for OIDC metadata.
|
|
# Enforced by the OCIS proxy service, not web — env var is PROXY_-prefixed.
|
|
PROXY_CSP_CONFIG_FILE_LOCATION: /etc/ocis/csp.yaml
|
|
# Expose the embedded NATS service registry and gRPC services on the Docker
|
|
# network so the collaboration container can talk to OCIS internals.
|
|
NATS_NATS_HOST: 0.0.0.0
|
|
NATS_NATS_PORT: 9233
|
|
GATEWAY_GRPC_ADDR: 0.0.0.0:9142
|
|
MICRO_GRPC_CLIENT_DNS_CACHE_TIMEOUT: 10s
|
|
# Audit service — JSON Lines to a shared volume that platform-api also
|
|
# mounts read-only. Used by the OCIS ingest worker to fold file/share
|
|
# mutations into the global audit timeline. The audit microservice is
|
|
# NOT part of the default `ocis server` set so we opt in explicitly.
|
|
OCIS_ADD_RUN_SERVICES: audit
|
|
AUDIT_LOG_FILE_PATH: /var/log/ocis/audit.log
|
|
AUDIT_LOG_FORMAT: json
|
|
volumes:
|
|
- ocis_config:/etc/ocis
|
|
- ocis_data:/var/lib/ocis
|
|
- ocis_audit_log:/var/log/ocis
|
|
- ./configs/ocis/csp.yaml:/etc/ocis/csp.yaml:ro
|
|
networks: [dezky]
|
|
depends_on:
|
|
- authentik-server
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.ocis.rule=Host(`files.dezky.local`)
|
|
- traefik.http.routers.ocis.tls=true
|
|
- traefik.http.services.ocis.loadbalancer.server.port=9200
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Collabora — Office document editor (loaded inside an iframe by OCIS)
|
|
# ─────────────────────────────────────────────────────────────────
|
|
collabora:
|
|
image: collabora/code:latest
|
|
container_name: dezky-collabora
|
|
restart: unless-stopped
|
|
cap_add:
|
|
- MKNOD
|
|
environment:
|
|
# Allow the WOPI host (OCIS collaboration service) to talk to Collabora
|
|
aliasgroup1: https://collaboration.dezky.local:443
|
|
DONT_GEN_SSL_CERT: "true"
|
|
# frame_ancestors: which hosts may embed Collabora in an iframe.
|
|
# Without this set, browsers block the iframe with a Danish "Indhold blokeret".
|
|
# home_mode.enable=true disables the "Explore The New" welcome popup AND the
|
|
# feedback prompt; caps at 20 connections / 10 documents (fine for dev).
|
|
extra_params: --o:ssl.enable=false --o:ssl.termination=true --o:ssl.ssl_verification=false --o:home_mode.enable=true --o:feedback.show=false --o:net.frame_ancestors=files.dezky.local
|
|
username: admin
|
|
password: ${COLLABORA_ADMIN_PASSWORD}
|
|
# Generate a fresh WOPI proof key on every start so the public key Collabora
|
|
# advertises matches the private key it signs with. Without this, the OCIS
|
|
# collaboration service rejects WOPI calls with "ProofKeys verification failed".
|
|
entrypoint: ["/bin/bash", "-c"]
|
|
command: ["coolconfig generate-proof-key && /start-collabora-online.sh"]
|
|
networks: [dezky]
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.collabora.rule=Host(`office.dezky.local`)
|
|
- traefik.http.routers.collabora.tls=true
|
|
- traefik.http.services.collabora.loadbalancer.server.port=9980
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# OCIS Collaboration — WOPI bridge between OCIS storage and Collabora.
|
|
# Shares the ocis_config volume so it picks up the same JWT/transfer secrets
|
|
# that ocis init generated. Without this, opening a .docx just downloads.
|
|
# ─────────────────────────────────────────────────────────────────
|
|
collaboration:
|
|
image: owncloud/ocis:7.0
|
|
container_name: dezky-collaboration
|
|
restart: unless-stopped
|
|
entrypoint: ["/bin/sh", "-c"]
|
|
command: ["ocis collaboration server"]
|
|
environment:
|
|
OCIS_URL: https://files.dezky.local
|
|
OCIS_LOG_LEVEL: warn
|
|
OCIS_INSECURE: "true"
|
|
OCIS_CONFIG_DIR: /etc/ocis
|
|
OCIS_BASE_DATA_PATH: /var/lib/ocis
|
|
COLLABORATION_HTTP_ADDR: 0.0.0.0:9300
|
|
COLLABORATION_GRPC_ADDR: 0.0.0.0:9301
|
|
COLLABORATION_WOPI_SRC: https://collaboration.dezky.local
|
|
COLLABORATION_APP_NAME: Collabora
|
|
COLLABORATION_APP_PRODUCT: Collabora
|
|
COLLABORATION_APP_DESCRIPTION: Collabora Online
|
|
# APP_ADDR is sent to the browser as the iframe src — must be public, not the
|
|
# Docker-internal hostname. Collaboration → Collabora traffic goes via Traefik.
|
|
COLLABORATION_APP_ADDR: https://office.dezky.local
|
|
COLLABORATION_APP_INSECURE: "true"
|
|
COLLABORATION_CS3API_DATAGATEWAY_INSECURE: "true"
|
|
# Match OCIS JWT/secrets via the shared config volume.
|
|
# Service registry: connect to OCIS's embedded NATS (not localhost).
|
|
MICRO_REGISTRY: nats-js-kv
|
|
MICRO_REGISTRY_ADDRESS: ocis:9233
|
|
# Direct gRPC pointer to OCIS's gateway (collaboration can't rely on registry
|
|
# lookups returning the right hostname for cross-container traffic).
|
|
REVA_GATEWAY: ocis:9142
|
|
volumes:
|
|
- ocis_config:/etc/ocis
|
|
- ocis_data:/var/lib/ocis
|
|
networks: [dezky]
|
|
depends_on:
|
|
- ocis
|
|
- collabora
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.collaboration.rule=Host(`collaboration.dezky.local`)
|
|
- traefik.http.routers.collaboration.tls=true
|
|
- traefik.http.services.collaboration.loadbalancer.server.port=9300
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Portal — Nuxt 3 customer portal (development mode with HMR)
|
|
# ─────────────────────────────────────────────────────────────────
|
|
portal:
|
|
image: node:20-alpine
|
|
container_name: dezky-portal
|
|
restart: unless-stopped
|
|
working_dir: /app
|
|
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
|
environment:
|
|
NODE_ENV: development
|
|
NUXT_HOST: 0.0.0.0
|
|
NUXT_PORT: 3000
|
|
NUXT_PUBLIC_AUTH_URL: https://auth.dezky.local
|
|
NUXT_PUBLIC_PORTAL_URL: https://app.dezky.local
|
|
NUXT_API_BASE: http://platform-api:3001
|
|
PLATFORM_API_INTERNAL_URL: http://platform-api:3001
|
|
MONGODB_URI: mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/dezky?authSource=admin
|
|
# OIDC (confidential client) — used by Nuxt server middleware
|
|
NUXT_OIDC_CLIENT_ID: ${PORTAL_OIDC_CLIENT_ID}
|
|
NUXT_OIDC_CLIENT_SECRET: ${PORTAL_OIDC_CLIENT_SECRET}
|
|
NUXT_OIDC_ISSUER: ${PORTAL_OIDC_ISSUER}
|
|
NUXT_OIDC_REDIRECT_URI: https://app.dezky.local/auth/oidc/callback
|
|
# Session encryption (required by nuxt-oidc-auth)
|
|
NUXT_OIDC_TOKEN_KEY: ${NUXT_OIDC_TOKEN_KEY}
|
|
NUXT_OIDC_SESSION_SECRET: ${NUXT_OIDC_SESSION_SECRET}
|
|
NUXT_OIDC_AUTH_SESSION_SECRET: ${NUXT_OIDC_AUTH_SESSION_SECRET}
|
|
# Trust mkcert root CA for Node fetch (dev only)
|
|
NODE_EXTRA_CA_CERTS: /etc/ssl/mkcert-root.pem
|
|
volumes:
|
|
- ../../apps/portal:/app
|
|
- portal_node_modules:/app/node_modules
|
|
- ./certs/mkcert-root.pem:/etc/ssl/mkcert-root.pem:ro
|
|
networks: [dezky]
|
|
depends_on:
|
|
mongo:
|
|
condition: service_healthy
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.portal.rule=Host(`app.dezky.local`) || Host(`dezky.local`)
|
|
- traefik.http.routers.portal.tls=true
|
|
- traefik.http.services.portal.loadbalancer.server.port=3000
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# Operator portal — internal admin app at operator.dezky.local.
|
|
# Separate from the customer portal: own OAuth client (dezky-operator),
|
|
# own session secrets, own cookie domain. Audience-gated mutations on
|
|
# platform-api require the token this app mints.
|
|
# ─────────────────────────────────────────────────────────────────
|
|
operator:
|
|
image: node:20-alpine
|
|
container_name: dezky-operator
|
|
restart: unless-stopped
|
|
working_dir: /app
|
|
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev"
|
|
environment:
|
|
NODE_ENV: development
|
|
NUXT_HOST: 0.0.0.0
|
|
NUXT_PORT: 3000
|
|
NUXT_PUBLIC_AUTH_URL: https://auth.dezky.local
|
|
# OIDC — dezky-operator OAuth client (separate from dezky-portal)
|
|
NUXT_OIDC_CLIENT_ID: ${OPERATOR_OIDC_CLIENT_ID}
|
|
NUXT_OIDC_CLIENT_SECRET: ${OPERATOR_OIDC_CLIENT_SECRET}
|
|
NUXT_OIDC_ISSUER: ${OPERATOR_OIDC_ISSUER}
|
|
NUXT_OIDC_REDIRECT_URI: https://operator.dezky.local/auth/oidc/callback
|
|
# Session encryption — distinct from portal so the two surfaces can't
|
|
# decrypt each other's session cookies
|
|
NUXT_OIDC_TOKEN_KEY: ${OPERATOR_NUXT_OIDC_TOKEN_KEY}
|
|
NUXT_OIDC_SESSION_SECRET: ${OPERATOR_NUXT_OIDC_SESSION_SECRET}
|
|
NUXT_OIDC_AUTH_SESSION_SECRET: ${OPERATOR_NUXT_OIDC_AUTH_SESSION_SECRET}
|
|
# Reach platform-api internally for the server-side token-forwarding proxy
|
|
PLATFORM_API_INTERNAL_URL: http://platform-api:3001
|
|
NODE_EXTRA_CA_CERTS: /etc/ssl/mkcert-root.pem
|
|
volumes:
|
|
- ../../apps/operator:/app
|
|
- operator_node_modules:/app/node_modules
|
|
- ./certs/mkcert-root.pem:/etc/ssl/mkcert-root.pem:ro
|
|
networks: [dezky]
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.operator.rule=Host(`operator.dezky.local`)
|
|
- traefik.http.routers.operator.tls=true
|
|
- traefik.http.services.operator.loadbalancer.server.port=3000
|
|
|
|
# ─────────────────────────────────────────────────────────────────
|
|
# platform-api — NestJS service. Owns tenants, partners, users,
|
|
# subscriptions, and provisioning orchestration.
|
|
# ─────────────────────────────────────────────────────────────────
|
|
platform-api:
|
|
image: node:20-alpine
|
|
container_name: dezky-platform-api
|
|
restart: unless-stopped
|
|
working_dir: /app
|
|
command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm start:dev"
|
|
environment:
|
|
NODE_ENV: development
|
|
PORT: 3001
|
|
MONGODB_URI: mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/dezky?authSource=admin
|
|
AUTHENTIK_API_URL: https://auth.dezky.local/api/v3
|
|
AUTHENTIK_API_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN}
|
|
STALWART_API_URL: https://mail.dezky.local
|
|
STALWART_ADMIN_USER: admin
|
|
STALWART_ADMIN_PASSWORD: ${STALWART_ADMIN_PASSWORD}
|
|
# HMAC secret Stalwart signs its webhook POSTs with; we verify on
|
|
# /ingest/stalwart/webhook. Both ends read the same env var.
|
|
STALWART_WEBHOOK_SECRET: ${STALWART_WEBHOOK_SECRET}
|
|
OCIS_API_URL: https://files.dezky.local
|
|
# JWT validation against Authentik for portal-issued access tokens.
|
|
# Issuers are comma-separated — each Authentik OAuth provider issues tokens
|
|
# with its own per-app issuer URL, so we accept both portal and operator.
|
|
AUTHENTIK_ISSUER: https://auth.dezky.local/application/o/dezky-portal/,https://auth.dezky.local/application/o/dezky-operator/
|
|
# Comma-separated list of accepted JWT audiences. Tokens issued for either
|
|
# the customer portal or the operator portal are valid against this service;
|
|
# per-endpoint guards further restrict operator-only mutations.
|
|
AUTHENTIK_AUDIENCE: dezky-portal,dezky-operator
|
|
AUTHENTIK_JWKS_URI: https://auth.dezky.local/application/o/dezky-portal/jwks/
|
|
# Trust mkcert root CA for Node fetch (dev only)
|
|
NODE_EXTRA_CA_CERTS: /etc/ssl/mkcert-root.pem
|
|
# Path to the OCIS audit log inside this container. The same shared
|
|
# volume is mounted on the OCIS service writeable; here it's read-only.
|
|
OCIS_AUDIT_LOG_PATH: /var/log/ocis/audit.log
|
|
volumes:
|
|
- ../../services/platform-api:/app
|
|
- platform_api_node_modules:/app/node_modules
|
|
- ./certs/mkcert-root.pem:/etc/ssl/mkcert-root.pem:ro
|
|
- ocis_audit_log:/var/log/ocis:ro
|
|
networks: [dezky]
|
|
depends_on:
|
|
mongo:
|
|
condition: service_healthy
|
|
labels:
|
|
- traefik.enable=true
|
|
- traefik.http.routers.api.rule=Host(`api.dezky.local`)
|
|
- traefik.http.routers.api.tls=true
|
|
- traefik.http.services.api.loadbalancer.server.port=3001
|
|
|