Kubernetes
Deploying routess with the bundled Helm chart.
The chart at charts/routess deploys all four apps (web, api, docs, landing) plus their services, ingresses, and security policies. Postgres is not managed by the chart; bring your own database.
For chart-level details, see charts/routess/README.md.
Prerequisites
- A Kubernetes cluster and
kubectlaccess - Helm 3.16+
- A reachable PostgreSQL instance (the chart does not create one)
- Secrets at hand: JWT secret, analytics salt, PAT pepper, Google OAuth credentials, database credentials
Install from GHCR
helm install routess oci://ghcr.io/robbeverhelst/charts/routess \
--version <VERSION> \
--namespace routess \
--create-namespace \
-f my-values.yamlFind the latest chart version on the releases page; the chart version tracks the app version.
Pick an ingress strategy
The default ingress.className is cloudflare-tunnel, which is not a standard ingress class; it assumes a Cloudflare Tunnel controller is installed. On any other setup, override it (the nginx overlay below sets className: nginx).
The chart ships with two example overlays:
Cloudflare Tunnel
helm install routess oci://ghcr.io/robbeverhelst/charts/routess \
-f charts/routess/values.example.cloudflare.yaml \
--set ingress.web.hosts[0]=routess.example.com \
--set ingress.api.hosts[0]=api.routess.example.com \
--set ingress.docs.hosts[0]=docs.routess.example.com \
--set networkPolicies.cloudflareNamespace=cloudflare \
...ingress-nginx + cert-manager
helm install routess oci://ghcr.io/robbeverhelst/charts/routess \
-f charts/routess/values.example.nginx.yaml \
--set ingress.web.hosts[0]=routess.example.com \
--set ingress.api.hosts[0]=api.routess.example.com \
--set ingress.docs.hosts[0]=docs.routess.example.com \
...Requires ingress-nginx, cert-manager, and a ClusterIssuer named letsencrypt-prod already present in the cluster.
Required secrets
Pass these from your secret manager (1Password Connect, External Secrets Operator, sealed-secrets, etc.):
--set api.secrets.jwtSecret=<random> \
--set api.secrets.analyticsSalt=<random> \
--set api.secrets.patPepper=<random> \
--set api.secrets.googleClientId=<from-google> \
--set api.secrets.googleClientSecret=<from-google> \
--set api.secrets.dbHost=<postgres-host> \
--set api.secrets.dbUser=<postgres-user> \
--set api.secrets.dbPassword=<postgres-password> \
--set networkPolicies.postgresEgress.host=<postgres-host>The API refuses to start without JWT_SECRET, ANALYTICS_SALT, and PAT_PEPPER. See ADR-0022.
Two related values live under api.env, not api.secrets: api.env.dbName (default routess) and api.env.dbPort (default "5432"). Override them if your database differs. api.secrets.resendApiKey is optional; empty means verification emails are logged to stdout instead of sent.
Rotation impact: rotating jwtSecret logs every user out, rotating patPepper invalidates every personal access token, and rotating analyticsSalt resets analytics pseudonyms.
Values structure
Top-level keys:
global: namespace, shared Umami analytics configweb,api,docs,landing: per-app deployment config (replicas, resources, probes, security)ingress: host configuration per servicenetworkPolicies: default-deny + app-specific allows (ADR-0006)monitoring.serviceMonitor: Prometheus Operator integrationserviceAccount: minimal RBAC per service
See values.yaml for the full reference.
Probes and disruption budgets
The API exposes GET /health/live (process up) and GET /health/ready (database reachable); the chart wires them as liveness and readiness probes. Web, docs, and landing probe GET /.
PodDisruptionBudgets keep at least one replica of web, api, and landing during node drains (podDisruptionBudget.minAvailable: 1); docs runs a single replica with no PDB.
Scaling
Each app ships an optional HorizontalPodAutoscaler: set <app>.autoscaling.enabled=true (defaults: min 2, max 5, target 80% CPU; docs scales 1 to 3). Web, docs, and landing are stateless and scale freely.
For the API, remember each pod opens its own PostgreSQL connections; raising maxReplicas multiplies connection count. If you scale wide, put a pooler (PgBouncer, or your managed Postgres's built-in pooling) in front of the database.
Swagger in production
The chart sets api.env.swaggerEnabled: "true", so interactive API docs are served at /api on the API host. The application default in production is off; set the value to "false" if you don't want them public.
Render locally
helm template test ./charts/routess \
--set api.secrets.jwtSecret=test \
--set api.secrets.analyticsSalt=test \
--set api.secrets.patPepper=test \
--set api.secrets.googleClientId=test \
--set api.secrets.googleClientSecret=test \
--set api.secrets.dbHost=10.0.0.1 \
--set api.secrets.dbUser=test \
--set api.secrets.dbPassword=testUpgrade
Snapshot the database first; migrations are forward-only and helm rollback does not undo schema changes. A pre-upgrade pg_dump is your rollback path.
helm upgrade routess oci://ghcr.io/robbeverhelst/charts/routess \
--version <NEW_VERSION> \
-n routess \
-f my-values.yamlThe API runs MikroORM migrations on startup. Standard rolling-update mechanics apply: the first new API pod migrates the schema, so watch its logs before letting the rest roll.