Kubernetes
cairn-core ships a self-contained kustomize bundle under
deploy/k8s/:
core, both workers and single-instance backing services. Its README.md is
the authoritative deploy guide; this page summarises it.
deploy/k8s/
README.md deploy steps, scaling, hardening
namespace.yaml cairn namespace
config.yaml non-secret config (ConfigMap cairn-config)
secrets.example.yaml copy to secrets.yaml and fill in; do not commit
postgres.yaml TimescaleDB StatefulSet + headless Service + 20Gi PVC
nats.yaml NATS + JetStream StatefulSet (max_payload 8MB) + 5Gi PVC
minio.yaml MinIO StatefulSet + 20Gi PVC (the server creates the bucket)
migrate-job.yaml one-shot `cairn migrate up`
core.yaml cairn-core Deployment + Service (:80 -> :8080)
worker-strava.yaml Strava worker Deployment
worker-garmin.yaml Garmin worker Deployment (emptyDir token cache)
ingress.yaml TLS entry point (nginx + cert-manager example)
kustomization.yaml ties them together + image tags
Deploy
-
Copy
secrets.example.yamltosecrets.yamland fill it in. GenerateCAIRN_AUTH_SESSION_SECRETandCAIRN_AUTH_MASTER_ENCRYPTION_KEYwithopenssl rand -base64 32. The DB password must be identical inCAIRN_DATABASE_URLandPOSTGRES_PASSWORD, the storage credentials identical toMINIO_ROOT_*. -
Set
CAIRN_HTTP_PUBLIC_BASE_URLinconfig.yamland the host iningress.yamlto your domain. They must match: OIDC redirects, webhook callbacks and share/reset links derive from it. -
Apply the secrets first (they are not part of the kustomization), then the bundle:
kubectl apply -f secrets.yamlkubectl apply -k .The
cairn-migrateJob runsmigrate upbefore core serves traffic; core refuses to start against an older schema. Re-running it is a no-op; after a version bump, delete and re-apply the Job. -
Verify with
kubectl -n cairn get pods,kubectl -n cairn logs job/cairn-migrateandkubectl -n cairn rollout status deploy/cairn-core, then log in at the Ingress host as the bootstrap admin (CAIRN_INSTANCE_BOOTSTRAP_ADMIN_EMAIL/..._PASSWORD).
Configuration is the CAIRN_* environment (see
Configuration), split between the ConfigMap and
the Secret.
State and scaling
- Workers reach core only through NATS (
nats:4222): job/result bus and blob presign request/reply. They need no DB or S3 credentials. - Core and the Strava worker are stateless; raise
replicasfreely, JetStream pull consumers share the work. Exception: the Strava webhook owner is single-instance. Keep the Deployment that setsCAIRN_STRAVA_WEBHOOK_VERIFY_TOKENatreplicas: 1; to scale Strava, run the pull workers without the token and one webhook-owner replica beside them. - The Garmin worker keeps a Garth token store under
CAIRN_GARTH_HOME. The bundle mounts it as anemptyDir, so a restarted pod logs in again from the credentials delivered over NATS. Mount a persistent volume there if MFA accounts should not be re-prompted on every restart (see Providers). Replicas do not share it. - Postgres, NATS and MinIO are single-instance in the bundle. For HA, swap in a Postgres operator (e.g. CloudNativePG), a NATS cluster and distributed MinIO or a managed S3 bucket, and point the same env at them.
Hardening
- The bundle runs NATS unauthenticated on the cluster network. For untrusted
networks enable enrollment-token auth-callout: bootstrap the account key
(
POST /api/admin/nats/bootstrap-account-key), put the returned public key innats.conf, and setCAIRN_WORKER_ENROLLMENT_TOKENon the Strava worker (see Providers). secrets.example.yamluses plainstringData; use SealedSecrets, External Secrets or SOPS for real clusters. Back upCAIRN_AUTH_MASTER_ENCRYPTION_KEYseparately from the database, it encrypts every credential at rest.- Terminate TLS at the Ingress and add a strict Content-Security-Policy there; it is not baked into the server.
- Backups: Postgres is the source of truth (including the encrypted NATS account key); MinIO holds re-fetchable raw blobs; JetStream is transient.
The worker images are built and published from their own repositories
(cairn-provider-strava, cairn-provider-garmin); the manifests just pin the
image tags.