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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user