# 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 → app.dezky.eu → booking.dezky.eu → ``` ## Deploy ```bash # 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.