chore: initial scaffold with running local stack and portal auth

Brings up Dezky's local development environment end-to-end:

Infrastructure (docker-compose):
- Traefik v3.7 reverse proxy with mkcert TLS (v3.2 couldn't speak Docker API 1.54)
- Postgres + Mongo + Redis with healthchecks and init script for per-service users
- Authentik 2025.10 (server + worker) as OIDC IdP
- Stalwart v0.16 mail server (image renamed from stalwartlabs/mail-server)
- OCIS 7.0 with PROXY_TLS=false and OCIS_CONFIG_DIR=/etc/ocis so init writes
  where the server reads
- Collabora office, plus the portal + provisioning service stubs
- Docker network aliases on Traefik so containers resolve auth.dezky.local etc.
  through the network (not host /etc/hosts)
- Docker socket mount parameterized for macOS Docker Desktop symlink path

Authentik provisioning (done via API after stack boot):
- ocis-provider (public client) + OCIS Files application
- dezky-portal provider (confidential) + Dezky Portal application
- Admin API token bound to akadmin manually since 2025.10's
  AUTHENTIK_BOOTSTRAP_TOKEN env var doesn't auto-materialize a token row

Portal (apps/portal):
- Nuxt 3 with nuxt-oidc-auth 1.0.0-beta.11 against generic 'oidc' preset
- Global auth middleware; login at /auth/oidc/login redirects to Authentik
- Visual implementation of Claude Design 'Auth' canvas: AuthShell, NodeMark,
  Auth* sub-components, design tokens as CSS custom properties
- Pages: auth/login, auth/expired, auth/disabled, index (post-login landing)
- mkcert root CA mounted into the portal so Node fetch trusts Authentik's
  self-signed cert (NODE_EXTRA_CA_CERTS) — dev only

Docs:
- AUTHENTIK-SETUP.md updated with manual token bind + portal provider scripted
  alternative
- NEXT-STEPS.md: Phase 1 and Phase 2 marked done with file locations and
  dev-mode caveats

Dev-mode shortcuts that need to be revisited before prod:
- skipAccessTokenParsing on the OIDC config
- NODE_EXTRA_CA_CERTS mkcert mount
- Bootstrap password still the generated value in .env
- Authentik admin token (dezky-bootstrap-token) is non-expiring
This commit is contained in:
Ronni Baslund
2026-05-23 21:25:11 +02:00
commit adfd9baafe
38 changed files with 14705 additions and 0 deletions
@@ -0,0 +1,341 @@
# 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
- files.dezky.local
- mail.dezky.local
- office.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
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
volumes:
- ocis_config:/etc/ocis
- ocis_data:/var/lib/ocis
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 (integrated into OCIS)
# ─────────────────────────────────────────────────────────────────
collabora:
image: collabora/code:latest
container_name: dezky-collabora
restart: unless-stopped
environment:
aliasgroup1: https://files\\.dezky\\.local:443
DONT_GEN_SSL_CERT: "true"
extra_params: --o:ssl.enable=false --o:ssl.termination=true --o:welcome.enable=false
username: admin
password: ${COLLABORA_ADMIN_PASSWORD}
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
# ─────────────────────────────────────────────────────────────────
# 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
volumes:
- ../../services/provisioning:/app
- provisioning_node_modules:/app/node_modules
networks: [dezky]
depends_on:
mongo:
condition: service_healthy