A root-run z-push-admin (kubectl exec defaults to root) left a root-owned 'users' file on the state PVC; Apache runs as www-data, so every request 500'd with 'Not possible to write to the configured state directory'. An initContainer now normalizes ownership on every start (state is disposable, ownership isn't precious), and the docs say to exec z-push-admin as www-data.
7.5 KiB
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:
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:
# 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:
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:
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_PASSWORDfrom.env
API:
- Base: https://auth.dezky.local/api/v3
- Auth:
Authorization: Bearer <AUTHENTIK_BOOTSTRAP_TOKEN> - Docs: https://auth.dezky.local/api/v3/
Debug:
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 comes
from the separate zpush gateway, see below)
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_PASSWORDfrom.env
Debug:
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
Z-Push (EAS gateway)
Image: built from services/zpush (Z-Push 2.6.4, AGPLv3 — see
services/zpush/LICENSE-NOTES.md)
Container: dezky-zpush
URL: https://mail.dezky.local/Microsoft-Server-ActiveSync (+ EAS
autodiscover on https://autodiscover.dezky.local)
Purpose: Exchange ActiveSync gateway in front of Stalwart — "Exchange"
accounts on iOS/Android native Mail/Calendar get two-way mail + calendar
sync (IMAP + CalDAV fan-out via BackendCombined). Contacts are NOT bundled
yet: the combined login is all-or-nothing and Stalwart 404s addressbook
homes that were never created — re-enable CardDAV once platform-api
provisions a default address book at mailbox creation.
Protocol reality check: EAS 14.1. Covers native mobile clients; NOT the
Outlook mobile app (requires EAS 16.1) and not new Outlook for Windows (no
EAS at all). Classic Outlook on Windows syncs calendars against /dav with
the free Outlook CalDAV Synchronizer add-in instead.
Auth: pure passthrough — the device's Basic credentials (mailbox
password or app password) go straight to Stalwart. No secrets stored;
zpush_state volume holds only resyncable device state.
Debug:
docker compose logs -f zpush
# Unauthenticated probe (expect 401 with realm="ZPush")
curl -k -i -X OPTIONS https://mail.dezky.local/Microsoft-Server-ActiveSync
# Authenticated: advertised EAS versions in MS-ASProtocolVersions header
curl -k -i -u user@tenant.tld:app-password -X OPTIONS \
https://mail.dezky.local/Microsoft-Server-ActiveSync
# Per-device sync state. ALWAYS run as www-data — a root-run z-push-admin
# leaves root-owned state files that 500 every request ("Not possible to
# write to the configured state directory"). The prod pod has an
# initContainer that re-chowns the state dir on start as a backstop.
docker exec -u www-data dezky-zpush php /usr/share/z-push/z-push-admin.php -a list
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_PASSWORDfrom.env
Storage backend:
- Dev: local filesystem inside volume
dezky_ocis_data - Prod: will switch to S3 (Hetzner Object Storage)
Debug:
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:
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: platform-api URL (server-side)MONGODB_URI: MongoDB connection string
Debug:
docker compose logs -f portal
# Shell into container
docker compose exec portal sh
> pnpm dev
Platform API (NestJS)
Container: dezky-platform-api
Port: 3001 (also exposed via Traefik at api.dezky.local)
Source: services/platform-api/
Purpose: Platform control plane — tenants, partners, users, subscriptions, provisioning orchestration, billing webhooks
Endpoints to implement:
POST /tenants— Create tenantGET /tenants/:id— Get tenantPATCH /tenants/:id— Update tenantPOST /tenants/:id/users— Add user to tenantPOST /webhooks/stripe— Billing events
Environment:
MONGODB_URI: Portal data storeAUTHENTIK_API_URL+AUTHENTIK_API_TOKENSTALWART_API_URL+STALWART_ADMIN_USER/PASSWORDOCIS_API_URL
Debug:
docker compose logs -f platform-api
# Test health endpoint
docker compose exec platform-api wget -qO- http://localhost:3001/health