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,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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
```
|
||||
@@ -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/
|
||||
```
|
||||
Reference in New Issue
Block a user