# dezky production — fleet (k3s app tier) > Pipeline verified end-to-end 2026-06-10: push to main → build → deploy. 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 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` | | operator | `git.lastcloud.io/ronnibaslund/dezky/operator` | `operator.dezky.eu` | `operator.dezky-apps:3000` | All of them 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 ├── redirect-middleware.yaml # per-router HTTP→HTTPS redirect (ACME-safe) ├── 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) ├── operator.yaml # Deployment + Service + Ingress (operator.dezky.eu) └── secrets.example.yaml # SECRET TEMPLATE — never commit real values ci/ ├── gitea-runner.yaml # in-cluster Gitea Actions runner (+ dind) └── ci-deployer.yaml # ServiceAccount/RBAC the CI deploy job uses ``` ## 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 **Push to main = release.** CI (`.gitea/workflows/ci.yml`, runner in-cluster — see `ci/gitea-runner.yaml`) typechecks + tests, builds each app image tagged `:latest` and the commit SHA, pushes to the Gitea registry, then the deploy job pins the kustomization to that SHA (`kustomize edit set image`), runs `kubectl apply -k apps/` and waits for the rollouts. No GitOps controller, no bot commits — the pipeline that built the image deploys it. One-time bootstrap for the deploy job's cluster access: ```bash # 1) ServiceAccount + RBAC (admin scoped to dezky-apps). kubectl apply -f ci/ci-deployer.yaml # 2) Mint a kubeconfig from its token and store it as the Gitea repo secret # KUBECONFIG_B64 (repo Settings → Actions → Secrets). API server address is # the in-cluster service IP — the runner's jobs run inside the cluster. TOKEN=$(kubectl -n dezky-apps get secret ci-deployer-token -o jsonpath='{.data.token}' | base64 -d) CA=$(kubectl -n dezky-apps get secret ci-deployer-token -o jsonpath='{.data.ca\.crt}') cat < **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.