Files
dezky/infrastructure/production/fleet
Ronni Baslund 326b626fc6
ci / typecheck (map[dir:services/platform-api name:platform-api]) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / typecheck (map[dir:apps/portal name:portal]) (push) Has been cancelled
ci / typecheck (map[dir:apps/website name:website]) (push) Has been cancelled
ci / typecheck (map[dir:apps/booking name:booking]) (push) Has been cancelled
feat(infra): full dezky rebrand of Authentik login (logo, favicon, bg, footer)
Brand CSS only reaches the flow shadow DOM via CSS vars (colors), not the
logo/favicon (deeper shadow root) or the "Powered by authentik" footer (light
DOM). So, dev-style: serve real dezky assets + sed the bundle.

- web-assets/: dezky-logo.svg, dezky-favicon.svg, dezky-bg.svg (carbon).
- server-rebrand.py: patches the authentik-server Deployment with an
  initContainer that copies /web/dist to an emptyDir, drops the svgs into
  assets/icons, and seds "Powered by authentik" -> "Powered by Dezky".
- brand.yaml: branding_logo / branding_favicon / branding_default_flow_background
  point at the served svgs; auth-flow title "Welcome to Dezky"; signal-green CSS.

Verified live: login now matches dev (logo, title, carbon bg, green button,
favicon, Powered by Dezky). Durability caveat documented (reverts on helm
upgrade).
2026-06-08 20:36:01 +02:00
..

dezky production — fleet (k3s app tier)

k3s manifests for the dezky application tier that runs in-cluster on the Hetzner AX41 node (see ../host/README.md for the host layer). This layer deploys the three first-party apps:

App Image Public host Internal Service
platform-api git.lastcloud.io/ronnibaslund/dezky/platform-api api.dezky.eu platform-api.dezky-apps:3001
portal git.lastcloud.io/ronnibaslund/dezky/portal app.dezky.eu portal.dezky-apps:3000
booking git.lastcloud.io/ronnibaslund/dezky/booking booking.dezky.eu booking.dezky-apps:3000

All three live in the dezky-apps namespace. The data tier (Postgres/Mongo/ Redis), Authentik and OCIS are added by other parts of the fleet layer and live in their own namespaces; these manifests reference them by cluster DNS only.

Files

apps/
├── kustomization.yaml          # bundles the non-secret resources
├── namespace.yaml              # dezky-apps namespace
├── platform-api.yaml           # Deployment + Service + Ingress (api.dezky.eu)
├── platform-api-config.yaml    # non-secret ConfigMap (Stalwart URL, toggles)
├── portal.yaml                 # Deployment + Service + Ingress (app.dezky.eu)
├── booking.yaml                # Deployment + Service + Ingress (booking.dezky.eu)
└── secrets.example.yaml        # SECRET TEMPLATE — never commit real values

Prerequisites (other fleet layers)

These manifests assume the cluster already has:

  • Traefik ingress controller (ships with k3s) — ingressClassName: traefik.
  • cert-manager with a ClusterIssuer named letsencrypt-prod (HTTP-01). The Ingresses request TLS certs via the cert-manager.io/cluster-issuer annotation; cert-manager fills the named *-tls Secrets.
  • MongoDB reachable at mongo.dezky-data.svc.cluster.local:27017.
  • Authentik reachable publicly at https://auth.dezky.eu with OIDC clients provisioned for the portal.
  • Stalwart on the host, reachable from the pod CIDR at its node-internal IP on :8080 (JMAP management). Update the placeholder IP in platform-api-config.yaml.

DNS

Point these A/AAAA records at the AX41 public IP:

api.dezky.eu       → <AX41 IP>
app.dezky.eu       → <AX41 IP>
booking.dezky.eu   → <AX41 IP>

Deploy

# 1) Apply real Secrets out-of-band (NOT from git). Copy the template,
#    fill in values, apply — or render SealedSecrets from it.
cp apps/secrets.example.yaml /tmp/dezky-secrets.yaml
$EDITOR /tmp/dezky-secrets.yaml          # fill every REPLACE / PASSWORD
kubectl apply -f /tmp/dezky-secrets.yaml
rm /tmp/dezky-secrets.yaml

# 2) Apply the app tier.
kubectl apply -k apps/

# 3) Watch rollout + cert issuance.
kubectl -n dezky-apps rollout status deploy/platform-api
kubectl -n dezky-apps get ingress,certificate

Images are pushed by CI (.gitea/workflows/ci.yml) to the Gitea registry. The manifests reference :latest for convenience; for real releases, set the image tag to the commit SHA and bump it per deploy (or wire Fleet/ArgoCD to do it).

Required env / secrets

Non-secret config lives in platform-api-config.yaml (ConfigMap) and inline env: in each Deployment. Secrets are defined in secrets.example.yaml and must be supplied at deploy time:

platform-api (platform-api-secrets)

Key Purpose How to get it
MONGODB_URI Portal/app database connection From the in-cluster Mongo credentials
SCHEDULING_CREDENTIAL_KEY AES key encrypting stored scheduling creds openssl rand -hex 32
STALWART_ADMIN_PASSWORD JMAP management auth Same value as the host config.env
STALWART_WEBHOOK_SECRET Audit webhook HMAC Same value as the host config.env

ConfigMap (platform-api-config): STALWART_API_URL, STALWART_ADMIN_USER, STALWART_PROVISIONING_ENABLED. PORT and DEZKY_ENV are set inline.

portal (portal-secrets)

Key Purpose
NUXT_OIDC_CLIENT_ID / NUXT_OIDC_CLIENT_SECRET Authentik OIDC client
NUXT_OIDC_REDIRECT_URI https://app.dezky.eu/auth/callback
NUXT_OIDC_SESSION_SECRET Session encryption (openssl rand -hex 32)
NUXT_PUBLIC_AUTH_URL Public Authentik URL (login + full sign-out)

PLATFORM_API_INTERNAL_URL / NUXT_API_BASE / NUXT_PUBLIC_PORTAL_URL are set inline in portal.yaml.

booking (booking-secrets)

Key Purpose
NUXT_PUBLIC_TURNSTILE_SITE_KEY Cloudflare Turnstile site key (public, env-injected)

PLATFORM_API_INTERNAL_URL / NUXT_PUBLIC_SITE_URL are set inline in booking.yaml.

Never commit real secret values. secrets.example.yaml is a template and is deliberately excluded from kustomization.yaml. Manage real secrets via sealed-secrets / SOPS / the Rancher secret store.