# 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: portal_node_modules: provisioning_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 - 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} 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 volumes: - ocis_config:/etc/ocis - ocis_data:/var/lib/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://provisioning: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 # ───────────────────────────────────────────────────────────────── # Provisioning service — NestJS worker for tenant lifecycle # ───────────────────────────────────────────────────────────────── provisioning: image: node:20-alpine container_name: dezky-provisioning 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: http://authentik-server:9000/api/v3 AUTHENTIK_API_TOKEN: ${AUTHENTIK_BOOTSTRAP_TOKEN} STALWART_API_URL: http://stalwart:8080 STALWART_ADMIN_USER: admin STALWART_ADMIN_PASSWORD: ${STALWART_ADMIN_PASSWORD} OCIS_API_URL: http://ocis:9200 # JWT validation against Authentik for portal-issued access tokens AUTHENTIK_ISSUER: https://auth.dezky.local/application/o/dezky-portal/ AUTHENTIK_AUDIENCE: dezky-portal 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 volumes: - ../../services/provisioning:/app - provisioning_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.api.rule=Host(`api.dezky.local`) - traefik.http.routers.api.tls=true - traefik.http.services.api.loadbalancer.server.port=3001