Skip to main content
Version: 0.7.2

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​

  1. Copy secrets.example.yaml to secrets.yaml and fill it in. Generate CAIRN_AUTH_SESSION_SECRET and CAIRN_AUTH_MASTER_ENCRYPTION_KEY with openssl rand -base64 32. The DB password must be identical in CAIRN_DATABASE_URL and POSTGRES_PASSWORD, the storage credentials identical to MINIO_ROOT_*.

  2. Set CAIRN_HTTP_PUBLIC_BASE_URL in config.yaml and the host in ingress.yaml to your domain. They must match: OIDC redirects, webhook callbacks and share/reset links derive from it.

  3. Pin the images in kustomization.yaml. The bundle tagged v0.6.0 still pins core v0.2.6, Strava v0.2.5 and Garmin v0.2.2, so set newTag to the release you deploy; for this release:

    kustomization.yaml
    images:
    - name: ghcr.io/opencairn-org/cairn-core
    newTag: v0.6.0
    - name: ghcr.io/opencairn-org/cairn-provider-strava
    newTag: v0.6.0
    - name: ghcr.io/opencairn-org/cairn-provider-garmin
    newTag: v0.6.0

    or kustomize edit set image ghcr.io/opencairn-org/cairn-core:v0.6.0 and likewise for the workers.

  4. Apply the secrets first (they are not part of the kustomization), then the bundle:

    kubectl apply -f secrets.yaml
    kubectl apply -k .

    The cairn-migrate Job runs migrate up before 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.

  5. Verify with kubectl -n cairn get pods, kubectl -n cairn logs job/cairn-migrate and kubectl -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 replicas freely, JetStream pull consumers share the work. Exception: the Strava webhook owner is single-instance. Keep the Deployment that sets CAIRN_STRAVA_WEBHOOK_VERIFY_TOKEN at replicas: 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 an emptyDir, 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 Garmin). Replicas do not share it. The image runs as uid/gid 10001; an emptyDir is writable for it, a persistent volume needs securityContext.fsGroup: 10001 on the pod (or a one-off chown -R 10001:10001 if it holds files from an older, root-run image).
  • 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 (core v0.3.1 or later):

    1. bootstrap the account key (POST /api/admin/nats/bootstrap-account-key);
    2. replace nats.conf in the nats-config ConfigMap with the auth-callout config and set CAIRN_NATS_AUTH_ISSUER (the returned public key) and CAIRN_NATS_PASSWORD on the NATS container, from a Secret;
    3. add CAIRN_NATS_USERNAME=cairn-core, CAIRN_NATS_PASSWORD, CAIRN_NATS_AUTH_CALLOUT=true and CAIRN_NATS_WORKER_ACCOUNT=CAIRN to core;
    4. create an enrollment (name and provider) per worker and set its CAIRN_WORKER_ENROLLMENT_TOKEN on the Strava and the Garmin worker (both v0.3.1 or later).

    Enrolled workers get the standard permission template.

  • secrets.example.yaml uses plain stringData; use SealedSecrets, External Secrets or SOPS for real clusters. Back up CAIRN_AUTH_MASTER_ENCRYPTION_KEY separately 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.

note

The worker images are built and published from their own repositories (cairn-provider-strava, cairn-provider-garmin); the manifests just pin the image tags.