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
+224
View File
@@ -0,0 +1,224 @@
# Authentik First-Time Setup
After the bootstrap script completes, Authentik is running but needs to be configured. This guide walks through the initial setup.
## 1. Access Authentik
Open https://auth.dezky.local in your browser.
If you see a TLS warning, mkcert root CA isn't trusted yet. Run:
```bash
mkcert -install
```
Then restart your browser.
## 2. Initial admin setup
Authentik bootstraps with admin credentials from `.env`:
- **URL:** https://auth.dezky.local/if/flow/initial-setup/
- **Email:** admin@dezky.local
- **Password:** Value of `AUTHENTIK_BOOTSTRAP_PASSWORD` in `.env`
On first login, change the password immediately.
## 3. Configure OIDC providers
Each Dezky service that uses SSO needs an OIDC provider configured in Authentik.
### 3.1 Create OCIS provider
1. Go to **Admin Interface****Applications****Providers**
2. Click **Create**
3. Select **OAuth2/OpenID Provider**
4. Configure:
- **Name:** `ocis-provider`
- **Authorization flow:** `default-provider-authorization-implicit-consent`
- **Client type:** Public
- **Client ID:** `ocis-web`
- **Redirect URIs:**
```
https://files.dezky.local/
https://files.dezky.local/oidc-callback
```
- **Signing Key:** `authentik Self-signed Certificate`
- **Scopes:** openid, profile, email
5. Save
### 3.2 Create OCIS application
1. Go to **Applications** → **Applications**
2. Click **Create**
3. Configure:
- **Name:** `OCIS Files`
- **Slug:** `ocis`
- **Provider:** `ocis-provider` (just created)
- **Launch URL:** https://files.dezky.local
4. Save
### 3.3 Create portal provider
Same steps as OCIS, but with:
- **Provider name:** `dezky-portal`
- **Client ID:** `dezky-portal`
- **Redirect URIs:** `https://app.dezky.local/api/auth/callback`
- **Client type:** Confidential (Authentik will generate a Client Secret)
Then create the matching application:
- **Name:** `Dezky Portal` → slug auto-generates as `dezky-portal`
- **Provider:** `dezky-portal` (from above)
- **Launch URL:** `https://app.dezky.local`
The resulting issuer URL is `https://auth.dezky.local/application/o/dezky-portal/` — note the slug includes `dezky-`.
After creating, copy the generated client secret into `.env`:
```
PORTAL_OIDC_CLIENT_ID=dezky-portal
PORTAL_OIDC_CLIENT_SECRET=<paste from Authentik provider page>
PORTAL_OIDC_ISSUER=https://auth.dezky.local/application/o/dezky-portal/
```
`docker-compose.yml` passes these to the portal container as `NUXT_OIDC_*`, which `nuxt-oidc-auth` (added in Phase 2) consumes.
### Scripted alternative
If you don't want to click through the UI, this one-shot uses the API token from section 4:
```bash
TOKEN=$(grep ^AUTHENTIK_BOOTSTRAP_TOKEN .env | cut -d= -f2)
BASE=https://auth.dezky.local/api/v3
AUTH="Authorization: Bearer $TOKEN"
# Pull the same authorization flow + signing key + scope mappings that OCIS uses
FLOW=$(curl -k -s -H "$AUTH" "$BASE/flows/instances/?slug=default-provider-authorization-implicit-consent" | jq -r '.results[0].pk')
KEY=$(curl -k -s -H "$AUTH" "$BASE/providers/oauth2/?search=ocis" | jq -r '.results[0].signing_key')
MAPS=$(curl -k -s -H "$AUTH" "$BASE/providers/oauth2/?search=ocis" | jq -c '.results[0].property_mappings')
# Create provider
PK=$(curl -k -s -X POST -H "$AUTH" -H "Content-Type: application/json" "$BASE/providers/oauth2/" -d "{
\"name\": \"dezky-portal\",
\"client_id\": \"dezky-portal\",
\"client_type\": \"confidential\",
\"authorization_flow\": \"$FLOW\",
\"signing_key\": \"$KEY\",
\"redirect_uris\": [{\"matching_mode\": \"strict\", \"url\": \"https://app.dezky.local/api/auth/callback\"}],
\"property_mappings\": $MAPS,
\"sub_mode\": \"hashed_user_id\",
\"issuer_mode\": \"per_provider\"
}" | jq -r '.pk')
# Create app
curl -k -s -X POST -H "$AUTH" -H "Content-Type: application/json" "$BASE/core/applications/" -d "{
\"name\": \"Dezky Portal\",
\"slug\": \"dezky-portal\",
\"provider\": $PK,
\"meta_launch_url\": \"https://app.dezky.local\"
}" >/dev/null
# Read the generated secret and write to .env
SECRET=$(curl -k -s -H "$AUTH" "$BASE/providers/oauth2/$PK/" | jq -r '.client_secret')
cat >> .env <<EOF
PORTAL_OIDC_CLIENT_ID=dezky-portal
PORTAL_OIDC_CLIENT_SECRET=$SECRET
PORTAL_OIDC_ISSUER=https://auth.dezky.local/application/o/dezky-portal/
EOF
```
### 3.4 Create Stalwart provider (when wiring mail SSO later)
Note: Stalwart's OIDC integration is configured in `infrastructure/docker-compose/configs/stalwart/config.toml`. For local dev with internal users, OIDC is optional.
## 4. Get the API token for provisioning service
The provisioning service needs to call Authentik's API to create tenants, users, and applications. `.env` holds a pre-generated value in `AUTHENTIK_BOOTSTRAP_TOKEN`, but Authentik 2025.10 does **not** materialize that env var into a usable API token on first boot. You need to create the token once and bind it to `akadmin`.
### One-time setup
Run this from the project root — it reads the value from `.env` and inserts it as a non-expiring API token for `akadmin`:
```bash
TOKEN=$(grep ^AUTHENTIK_BOOTSTRAP_TOKEN .env | cut -d= -f2)
docker compose -f infrastructure/docker-compose/docker-compose.yml exec -T \
-e BOOTSTRAP_TOKEN="$TOKEN" authentik-server ak shell -c "
import os
from authentik.core.models import User, Token, TokenIntents
admin = User.objects.get(username='akadmin')
Token.objects.update_or_create(
identifier='dezky-bootstrap-token',
defaults={
'user': admin,
'intent': TokenIntents.INTENT_API,
'expiring': False,
'key': os.environ['BOOTSTRAP_TOKEN'],
},
)
print('Token bound to akadmin')
"
```
Alternative: create the token through the UI — **Directory → Tokens & App passwords → Create**, set `Intent: API`, `User: akadmin`, then copy the key into `.env` and restart the provisioning service.
### Verify it works
```bash
curl -k -H "Authorization: Bearer $(grep ^AUTHENTIK_BOOTSTRAP_TOKEN .env | cut -d= -f2)" \
https://auth.dezky.local/api/v3/core/users/
```
A 200 response with the user list (including `akadmin`) means the token is live. A 403 `{"detail":"Token invalid/expired"}` means the one-time setup above hasn't been run yet.
### When you need to re-run this
After `docker compose down -v` (or any reset that wipes the postgres volume), the token row is gone and you'll need to recreate it. The value in `.env` doesn't need to change — re-running the shell snippet above re-binds it.
## 5. Multi-tenancy strategy
For local dev, you can either:
**Option A: Single Authentik tenant, multiple groups**
- All Dezky test users in one Authentik instance
- Tenants modeled as Authentik groups
- Simpler for dev, less realistic
**Option B: Authentik tenants (production-mode)**
- Each Dezky customer = Authentik tenant
- Tenant subdomain pattern: `{tenant}.auth.dezky.local`
- More realistic but more setup overhead
For dev, start with Option A. The provisioning service should be built to support Option B from day one (data model includes `tenantId`).
## 6. Test SSO flow end-to-end
1. Open incognito browser to https://files.dezky.local
2. OCIS should redirect to https://auth.dezky.local for login
3. Log in with admin@dezky.local
4. Should redirect back to OCIS, logged in
If this works, OIDC integration is solid.
## Common issues
### "Issuer URL does not match"
OCIS expects exact match between `OCIS_OIDC_ISSUER` and the `iss` claim in the JWT.
Check the issuer in Authentik:
- Admin Interface → Providers → ocis-provider → Configure
- Note the **Issuer URL** at the bottom
- Update `OCIS_OIDC_ISSUER` in `docker-compose.yml` to match exactly
### "Client authentication failed"
Public clients (Client Type: Public) don't need a client secret.
Confidential clients need the secret added to the consumer's config.
For OCIS, use Public type.
For the portal (which has server-side auth), use Confidential.
### TLS verification fails between containers
Inside the Docker network, services use Docker-internal hostnames (e.g. `authentik-server`). TLS verification can fail because the cert is for `auth.dezky.local`, not `authentik-server`.
For service-to-service auth, use the issuer URL with `OCIS_INSECURE=true` only for dev. Production will use proper certs.
+163
View File
@@ -0,0 +1,163 @@
# Next Steps — After Local Stack Is Running
Once `./scripts/bootstrap.sh` completes successfully and all services are reachable, here's the development roadmap.
## Phase 1: Verify everything works (day 1) — done
- [x] `https://app.dezky.local` shows portal landing page (now the new auth design / post-login home)
- [x] `https://auth.dezky.local` shows Authentik login
- [x] Log into Authentik as admin *(still using generated `AUTHENTIK_BOOTSTRAP_PASSWORD` from `.env` — rotate before exposing to anyone else)*
- [x] Follow `docs/AUTHENTIK-SETUP.md` to configure OIDC providers (ocis + dezky-portal)
- [x] Test OCIS SSO end-to-end (login from `https://files.dezky.local`)
- [x] Verify Stalwart admin UI loads at `https://mail.dezky.local/login` *(root path 404s — admin SPA is at `/login`)*
## Phase 2: Build portal authentication (week 1) — done
Goal: Users can log in to the portal via Authentik.
- [x] Add `nuxt-oidc-auth` to `apps/portal` (`1.0.0-beta.11`)
- [x] Configure Authentik as OIDC provider (generic `oidc` preset with explicit URLs + discovery)
- [x] Implement login/logout flows (`/auth/oidc/login`, `/auth/oidc/logout` from the module)
- [x] Display logged-in user info on the portal home (`pages/index.vue` uses `useOidcAuth()`)
- [x] Add protected routes (`globalMiddlewareEnabled: true`; public pages opt out via `definePageMeta({ auth: false })`)
### Where things live
| Concern | File |
|---|---|
| OIDC module config | `apps/portal/nuxt.config.ts` (`oidc` block) |
| Custom login page | `apps/portal/pages/auth/login.vue` |
| Error states (expired / disabled) | `apps/portal/pages/auth/{expired,disabled}.vue` |
| Post-login landing | `apps/portal/pages/index.vue` |
| Visual shell + tokens | `apps/portal/components/auth/*`, `assets/styles/tokens.css` |
| Brand mark | `apps/portal/components/NodeMark.vue` |
### Dev-mode caveats (clean up before prod)
- `skipAccessTokenParsing: true` in the OIDC config — Authentik's access tokens in this setup aren't reliably JWT-parseable; production should re-evaluate
- `openIdConfiguration` is pinned to the discovery URL because the generic `oidc` preset doesn't ship a default — required for id_token JWKS validation
- `docker-compose.yml` mounts `infrastructure/docker-compose/certs/mkcert-root.pem` into the portal at `/etc/ssl/mkcert-root.pem` and sets `NODE_EXTRA_CA_CERTS` so Node fetch trusts the mkcert root CA. In prod, replace with real CA-signed certs
- Traefik has Docker network aliases for `auth.dezky.local`, `app.dezky.local`, etc. so container-to-Authentik fetch resolves inside the network without going through host `/etc/hosts`
## Phase 3: Tenant data model (week 1-2)
Goal: MongoDB schema for tenants, users, subscriptions.
- [ ] Define Mongoose schemas in `services/provisioning/src/schemas/`
- [ ] Tenant schema: id, name, slug, status, plan, billingInfo, domains
- [ ] User schema: id, tenantId, email, name, role, authentikId
- [ ] Subscription schema: tenantId, plan, status, stripeCustomerId
- [ ] Add CRUD endpoints in NestJS
Schema example:
```typescript
// services/provisioning/src/schemas/tenant.schema.ts
@Schema({ timestamps: true })
export class Tenant {
@Prop({ required: true, unique: true })
slug: string
@Prop({ required: true })
name: string
@Prop({ enum: ['pending', 'active', 'suspended', 'deleted'], default: 'pending' })
status: string
@Prop({ type: [String], default: [] })
domains: string[]
@Prop({ enum: ['mvp', 'pro', 'enterprise'], default: 'mvp' })
plan: string
@Prop()
authentikGroupId?: string
@Prop()
ocisSpaceId?: string
}
```
## Phase 4: Provisioning automation (week 2-3)
Goal: Sign up creates tenant resources across all services.
- [ ] Endpoint: `POST /tenants` — creates tenant in MongoDB
- [ ] Worker: triggers Authentik tenant/group creation via API
- [ ] Worker: configures Stalwart domain + DKIM via admin API
- [ ] Worker: creates OCIS space
- [ ] Worker: emails customer with onboarding info
Authentik API examples:
```typescript
// Create group (tenant) in Authentik
await authentikClient.coreGroupsCreate({
name: tenant.slug,
attributes: { tenantId: tenant.id, plan: tenant.plan },
})
// Create user
await authentikClient.coreUsersCreate({
username: user.email,
email: user.email,
name: user.name,
groups: [authentikGroupId],
})
```
## Phase 5: Custom webmail (week 3-4)
Goal: Branded webmail client using Stalwart's JMAP API.
- [ ] Add JMAP client library to portal
- [ ] Build inbox view in Nuxt
- [ ] Build compose dialog
- [ ] Build message view with thread support
- [ ] Style to match Dezky branding
JMAP is a modern JSON-RPC protocol — clean to work with.
## Phase 6: Production migration prep (week 4+)
When the local stack is solid and you have 2-3 pilot customers interested:
- [ ] Order Hetzner AX41-NVMe
- [ ] Order Storage Box BX11 (Falkenstein)
- [ ] Enable Hetzner Object Storage (bucket: dezky-ocis-prod)
- [ ] Build Terraform module for Hetzner provisioning
- [ ] Build Ansible playbook for bare-metal Stalwart deployment
- [ ] Set up k3s on the cloud server
- [ ] Migrate compose to Helm charts
- [ ] Configure Let's Encrypt via cert-manager
- [ ] Set up Restic backup jobs to Storage Box + B2
## Phase 7: Add Zulip and Jitsi (when chat/video needed)
These were excluded from MVP for simplicity. When ready:
- [ ] Create `infrastructure/docker-compose/docker-compose.optional.yml`
- [ ] Add Zulip stack (server + db + worker)
- [ ] Add Jitsi stack (web + prosody + jicofo + jvb)
- [ ] Configure OIDC integration with Authentik
- [ ] Add to portal launcher
## Decisions still open
These need to be made before public launch:
- [ ] Final pricing tiers (MVP, Pro, Enterprise)
- [ ] dezky.com purchase decision ($3,000 via BrandBucket)
- [ ] Final logo design (4 directions explored, need to pick one)
- [ ] Legal entity structure for the new business
- [ ] DPA (databehandleraftale) template
- [ ] Customer support process (ticket system choice)
## Long-term architecture goals
- [ ] Multi-region deployment (Hetzner Falkenstein + Helsinki)
- [ ] Disaster recovery: cross-DC Restic copies
- [ ] ISO 27001 certification via Vanta
- [ ] GDPR Article 30 record of processing activities
- [ ] SOC 2 (later, for enterprise customers)
- [ ] Customer-facing status page (Uptime Kuma or cstate)
- [ ] Public documentation site
- [ ] Self-service migration tooling from M365
+262
View File
@@ -0,0 +1,262 @@
# Services Reference
Per-service details: what each one does, where its config lives, and how to debug it.
## Traefik
**Image:** `traefik:v3.2`
**Container:** `dezky-traefik`
**URL:** https://traefik.dezky.local (dashboard)
**Purpose:** Reverse proxy, TLS termination, service discovery via Docker labels
**Config:**
- Static: `configs/traefik/traefik.yml`
- Dynamic: `configs/traefik/dynamic.yml` (TLS certs)
- Certs: `certs/dezky.local.pem` + `certs/dezky.local-key.pem`
**Debug:**
```bash
docker compose logs -f traefik
# Open https://traefik.dezky.local for dashboard
```
---
## PostgreSQL
**Image:** `postgres:16-alpine`
**Container:** `dezky-postgres`
**Internal hostname:** `postgres`
**Purpose:** Shared RDBMS for Authentik and OCIS (future)
**Databases:**
- `authentik` (owner: `authentik`)
- `ocis` (owner: `ocis`, reserved for future use)
**Debug:**
```bash
# Shell access
docker compose exec postgres psql -U postgres
# Check users
\du
# Check databases
\l
# Connect to specific DB
\c authentik
```
---
## MongoDB
**Image:** `mongo:7`
**Container:** `dezky-mongo`
**Internal hostname:** `mongo`
**Purpose:** Portal application data
**Connection:**
```
mongodb://root:${MONGO_ROOT_PASSWORD}@mongo:27017/dezky?authSource=admin
```
**Debug:**
```bash
docker compose exec mongo mongosh -u root -p $(grep MONGO_ROOT_PASSWORD .env | cut -d= -f2)
```
---
## Redis
**Image:** `redis:7-alpine`
**Container:** `dezky-redis`
**Internal hostname:** `redis`
**Purpose:** Cache and session store (used by Authentik)
**Debug:**
```bash
docker compose exec redis redis-cli -a $(grep REDIS_PASSWORD .env | cut -d= -f2)
> KEYS *
> INFO
```
---
## Authentik
**Image:** `ghcr.io/goauthentik/server:2025.10`
**Containers:** `dezky-authentik` (server) + `dezky-authentik-worker`
**URL:** https://auth.dezky.local
**Purpose:** Identity provider, SSO, MFA
**First-time setup:**
- URL: https://auth.dezky.local/if/flow/initial-setup/
- Email: `admin@dezky.local`
- Password: `AUTHENTIK_BOOTSTRAP_PASSWORD` from `.env`
**API:**
- Base: https://auth.dezky.local/api/v3
- Auth: `Authorization: Bearer <AUTHENTIK_BOOTSTRAP_TOKEN>`
- Docs: https://auth.dezky.local/api/v3/
**Debug:**
```bash
docker compose logs -f authentik-server authentik-worker
# Check API health
curl https://auth.dezky.local/-/health/ready/
```
See `docs/AUTHENTIK-SETUP.md` for OIDC configuration steps.
---
## Stalwart Mail
**Image:** `stalwartlabs/mail-server:latest`
**Container:** `dezky-stalwart`
**URL:** https://mail.dezky.local
**Purpose:** Mail server (SMTP/IMAP/JMAP/CalDAV/CardDAV/ActiveSync)
**Ports exposed:**
- 25 (SMTP)
- 465 (SMTPS)
- 587 (Submission)
- 143 (IMAP)
- 993 (IMAPS)
- 4190 (ManageSieve)
**Config:** `configs/stalwart/config.toml`
**Data:** Docker volume `dezky_stalwart_data`
**Admin login:**
- User: `admin`
- Password: `STALWART_ADMIN_PASSWORD` from `.env`
**Debug:**
```bash
docker compose logs -f stalwart
# Test SMTP
swaks --to test@dezky.local --from sender@example.com --server mail.dezky.local:25
# Check ports
docker compose port stalwart 25
```
---
## OCIS
**Image:** `owncloud/ocis:7.0`
**Container:** `dezky-ocis`
**URL:** https://files.dezky.local
**Purpose:** File storage, sharing, sync
**OIDC config:**
- Issuer: `https://auth.dezky.local/application/o/ocis/`
- Client ID: `ocis-web` (configured in Authentik)
- Auto-provision: enabled (creates OCIS user on first SSO login)
**Admin login:**
- User: `admin`
- Password: `OCIS_ADMIN_PASSWORD` from `.env`
**Storage backend:**
- Dev: local filesystem inside volume `dezky_ocis_data`
- Prod: will switch to S3 (Hetzner Object Storage)
**Debug:**
```bash
docker compose logs -f ocis
# Health check
curl -k https://files.dezky.local/
```
---
## Collabora
**Image:** `collabora/code:latest`
**Container:** `dezky-collabora`
**URL:** https://office.dezky.local
**Purpose:** Office document editing inside OCIS
**Integration with OCIS:**
- OCIS must be configured to use Collabora as its office editor
- See: OCIS app config → "wopiserver"
**Debug:**
```bash
docker compose logs -f collabora
# Discovery endpoint (used by OCIS)
curl -k https://office.dezky.local/hosting/discovery
```
---
## Portal (Nuxt 3)
**Container:** `dezky-portal`
**URL:** https://app.dezky.local
**Source:** `apps/portal/`
**Purpose:** Customer-facing portal, launcher, custom webmail
**Stack:**
- Nuxt 3
- Vue 3 + TypeScript
- Vite dev server
- pnpm for dependencies
**Hot reload:**
- File changes in `apps/portal/` trigger HMR automatically
- Vite watches via polling (configured in `nuxt.config.ts`)
**Environment:**
- `NUXT_PUBLIC_AUTH_URL`: Authentik URL (client-side)
- `NUXT_API_BASE`: provisioning service URL (server-side)
- `MONGODB_URI`: MongoDB connection string
**Debug:**
```bash
docker compose logs -f portal
# Shell into container
docker compose exec portal sh
> pnpm dev
```
---
## Provisioning Service (NestJS)
**Container:** `dezky-provisioning`
**Port:** 3001 (internal only)
**Source:** `services/provisioning/`
**Purpose:** Tenant lifecycle, billing webhooks, service orchestration
**Endpoints to implement:**
- `POST /tenants` — Create tenant
- `GET /tenants/:id` — Get tenant
- `PATCH /tenants/:id` — Update tenant
- `POST /tenants/:id/users` — Add user to tenant
- `POST /webhooks/stripe` — Billing events
**Environment:**
- `MONGODB_URI`: Portal data store
- `AUTHENTIK_API_URL` + `AUTHENTIK_API_TOKEN`
- `STALWART_API_URL` + `STALWART_ADMIN_USER/PASSWORD`
- `OCIS_API_URL`
**Debug:**
```bash
docker compose logs -f provisioning
# Test health endpoint
docker compose exec provisioning wget -qO- http://localhost:3001/health
```
+305
View File
@@ -0,0 +1,305 @@
# Troubleshooting
Common issues and fixes when running the Dezky local development stack.
## TLS / Certificate issues
### Browser shows "Not Secure" or certificate warning
mkcert root CA isn't trusted in your browser yet.
```bash
mkcert -install
```
Then **fully restart your browser** (quit, not just close window).
### Certificate not loading in Traefik
Verify the cert files exist:
```bash
ls -la infrastructure/docker-compose/certs/
# Should show:
# dezky.local.pem
# dezky.local-key.pem
```
If they're named differently (e.g. `_wildcard.dezky.local+1.pem`), rename them:
```bash
cd infrastructure/docker-compose/certs/
mv _wildcard.dezky.local+*.pem dezky.local.pem
mv _wildcard.dezky.local+*-key.pem dezky.local-key.pem
```
Then restart Traefik:
```bash
docker compose restart traefik
```
### Service-to-service TLS errors
Inside Docker, services talk via internal hostnames (e.g. `authentik-server:9000`), not `auth.dezky.local`. Internal traffic uses HTTP, not HTTPS. Only Traefik handles TLS termination.
If a service config has `https://authentik-server`, change it to `http://authentik-server`.
---
## Container startup issues
### Authentik fails to start
Most common cause: PostgreSQL not ready yet, or password mismatch.
```bash
# Check postgres is healthy
docker compose ps postgres
# Should show "healthy" in STATUS
# Check Authentik DB user exists
docker compose exec postgres psql -U postgres -c "\du"
# Should list "authentik" as a user
# Check Authentik logs
docker compose logs authentik-server | tail -50
```
If password is wrong, reset and re-bootstrap:
```bash
./scripts/reset.sh
./scripts/bootstrap.sh
```
### Port 25 conflict (Stalwart fails to bind)
macOS often has Postfix running by default:
```bash
sudo launchctl unload /System/Library/LaunchDaemons/org.postfix.master.plist 2>/dev/null || true
sudo launchctl stop org.postfix.master 2>/dev/null || true
```
Or just disable the SMTP port mapping in `docker-compose.yml` for local dev:
```yaml
stalwart:
# ports:
# - "25:25" # comment out if conflicting
```
### Port 80/443 conflict (Traefik fails to bind)
Another service is using those ports.
```bash
# Find what's using port 80
sudo lsof -nP -i:80 -sTCP:LISTEN
```
Common culprits: nginx, apache, Caddy, other Docker stacks. Stop them or change Traefik to use 8080/8443.
### OCIS crashes on first start
OCIS needs to initialize before running. The compose file does this via:
```yaml
command: ["-c", "ocis init --insecure true || true && ocis server"]
```
If init fails:
```bash
# Manually init
docker compose run --rm ocis ocis init --insecure true
# Then start
docker compose up -d ocis
```
---
## DNS / hostname issues
### `app.dezky.local` doesn't resolve
Check /etc/hosts:
```bash
grep dezky.local /etc/hosts
```
Should see entries pointing 127.0.0.1 to all hostnames. If missing, run:
```bash
./scripts/bootstrap.sh # Will offer to add them
```
Or manually:
```bash
echo "127.0.0.1 dezky.local app.dezky.local auth.dezky.local mail.dezky.local files.dezky.local office.dezky.local meet.dezky.local chat.dezky.local traefik.dezky.local" | sudo tee -a /etc/hosts
```
### Browser DNS cache holding old entry
Clear browser cache, or test from terminal:
```bash
ping app.dezky.local
# Should return 127.0.0.1
```
If terminal resolves but browser doesn't:
- Chrome: chrome://net-internals/#dns → Clear host cache
- Firefox: about:networking#dns → Clear DNS cache
---
## Authentik OIDC integration issues
### "Invalid issuer URL"
The `iss` claim in the JWT must match exactly what the consuming service expects.
```yaml
# In docker-compose.yml for OCIS:
OCIS_OIDC_ISSUER: https://auth.dezky.local/application/o/ocis/
```
The trailing slash matters. Authentik issues with trailing slash by default.
Verify the actual issuer:
```bash
curl -s https://auth.dezky.local/application/o/ocis/.well-known/openid-configuration | jq .issuer
```
### "redirect_uri not allowed"
The OAuth provider in Authentik must list every redirect URI the client might use.
For OCIS:
```
https://files.dezky.local/
https://files.dezky.local/oidc-callback
```
Add both. Patterns matter — exact match.
### Login loop (redirects forever)
Usually caused by:
1. **Time mismatch** between container and host. Check `docker compose exec ocis date` matches host clock.
2. **Cookie domain mismatch.** Cookies set for `.dezky.local` should work across subdomains.
---
## Hot reload not working
### Nuxt portal doesn't rebuild on file changes
The volume mount works on macOS but file watching needs explicit polling:
Add to `apps/portal/nuxt.config.ts`:
```ts
export default defineNuxtConfig({
vite: {
server: {
watch: {
usePolling: true,
interval: 1000,
},
},
},
})
```
### NestJS provisioning doesn't restart
Same issue. The `start:dev` command uses nodemon under the hood. Make sure your `package.json` has:
```json
{
"scripts": {
"start:dev": "nest start --watch"
}
}
```
---
## Data and reset issues
### Want to keep data but restart services
```bash
docker compose restart [service-name]
```
### Want to reset just one service
```bash
docker compose stop authentik-server authentik-worker
docker volume rm dezky_authentik_media dezky_authentik_certs
docker compose up -d authentik-server authentik-worker
```
### Full reset (nuclear option)
```bash
./scripts/reset.sh
./scripts/bootstrap.sh
```
---
## Performance issues
### Stack is using too much RAM
Check usage:
```bash
docker stats --no-stream
```
Top RAM consumers are usually:
- Zulip (4-6 GB) — disabled in main compose
- Jitsi (2-4 GB) — disabled in main compose
- Authentik server + worker (~1 GB each)
- OCIS (~1 GB)
- Collabora (1-2 GB if active document open)
For low-memory machines, disable services you're not using:
```bash
docker compose stop collabora # Save ~1 GB
docker compose stop ocis # Save ~1 GB if not testing files
```
### macOS Docker is slow
OrbStack is significantly faster than Docker Desktop on macOS:
```bash
brew install --cask orbstack
```
Or in Docker Desktop, enable VirtioFS for bind mount performance.
---
## Logs and debugging
### See logs from one service
```bash
docker compose logs -f authentik-server
```
### See logs from multiple services
```bash
docker compose logs -f authentik-server authentik-worker postgres
```
### Inspect a container
```bash
docker compose exec authentik-server sh
# or
docker compose exec postgres psql -U postgres
```
### See what's running
```bash
docker compose ps
```
### Network debugging — can services reach each other?
```bash
docker compose exec ocis ping -c 3 authentik-server
docker compose exec ocis curl -v http://authentik-server:9000/-/health/ready/
```