Self-host overview
Cairn is built to be self-hosted. A production deployment is:
- cairn-core — one distroless container. It serves the Connect-RPC + REST API and the embedded SvelteKit web UI, runs migrations, and is the server side of the NATS control plane.
- Postgres 17 with TimescaleDB (streams) and PostGIS (segments).
- NATS + JetStream — the worker control plane and async job/result bus.
- An S3-compatible object store (MinIO, Garage, AWS S3, …) for raw blobs and photos. Optional — core runs without it, you just lose blob upload.
- Provider workers — one container per provider you use
(
cairn-provider-strava,cairn-provider-garmin), each a separate process.
+-------------------+
web UI ───▶ | cairn-core | ──▶ Postgres (Timescale + PostGIS)
API ───▶ | (API + web UI) | ──▶ S3 (blobs, photos)
+---------+---------+
│ NATS + JetStream (jobs / results / tokens)
┌───────────┴───────────┐
▼ ▼
cairn-provider-strava cairn-provider-garmin
Choose a path
- Docker Compose — simplest single-host deployment.
- Kubernetes — manifests for a cluster.
- Configuration — environment variables.
- Providers — wiring Strava and Garmin.
Docker Compose
Pull the published images and run them together. Core embeds the web UI, so there is nothing else to serve.
services:
core:
image: ghcr.io/opencairn-org/cairn-core:v0.3.3
env_file: .env
ports: ["8080:8080"]
depends_on: [postgres, nats]
worker-strava:
image: ghcr.io/opencairn-org/cairn-provider-strava:v0.3.2
env_file: .env
# one enrollment per worker, only with NATS auth-callout
environment: { CAIRN_WORKER_ENROLLMENT_TOKEN: "${STRAVA_ENROLLMENT_TOKEN:-}" }
worker-garmin:
image: ghcr.io/opencairn-org/cairn-provider-garmin:v0.3.2
env_file: .env
environment: { CAIRN_WORKER_ENROLLMENT_TOKEN: "${GARMIN_ENROLLMENT_TOKEN:-}" }
volumes: ["garmin-tokens:/data"]
postgres:
image: timescale/timescaledb-ha:pg17-ts2.27
nats: { image: nats:2.15.1-alpine, command: ["-js"] }
minio: { image: minio/minio, command: ["server", "/data"] }
volumes:
garmin-tokens:
Pin the image tags to a release rather than latest, and upgrade core and the
workers together; this page documents core v0.3.3 with the Strava worker
v0.3.2 and the Garmin worker v0.3.2.
The Garmin worker runs as the unprivileged user cairn (uid/gid 10001) and
keeps its Garmin token cache under /data. A new named volume is created with
the right owner. A volume or bind mount written by an older image (which ran as
root) has to be handed over once, otherwise the cached tokens cannot be read:
docker run --rm -v garmin-tokens:/data alpine chown -R 10001:10001 /data
Put a TLS-terminating reverse proxy (Caddy, nginx, Traefik) in front of core,
and set CAIRN_INSTANCE_TRUSTED_PROXIES to its address so client IPs are read
from the forwarded headers. See Configuration for
the full variable list — in particular the three required secrets and the map
tile settings if you would rather not hit the public OpenStreetMap tile server
from your instance.
Upgrading to 0.3.3
- The project moved to the
opencairn-orgGitHub organization. Pull the images fromghcr.io/opencairn-org/(cairn-core,cairn-provider-strava,cairn-provider-garmin); theghcr.io/johnnycube/namespace keeps the tags published so far as a frozen mirror and receives no new versions. Update image pins and Renovate rules; nothing else to migrate.
Upgrading to 0.3.2
- Worker enrollments accept any well-formed provider type (1–48 of
[a-z0-9_-]), not onlystravaandgarmin; the admin UI suggests the built-in ones. Nothing to migrate. Account linking for a new provider still needs core support (see Other providers).
Upgrading to 0.3.1
- Creating a worker enrollment requires a
provider. Enrollments made before 0.3.1 have none; core refuses them at connect and the admin list marks them re-enroll required. Create new ones (see Providers). - Core answers NATS auth-callout requests only with
CAIRN_NATS_AUTH_CALLOUT=trueandCAIRN_NATS_WORKER_ACCOUNTset; follow the auth-callout setup. Without NATS auth nothing changes. - With the auth-callout, run Strava v0.3.1 and Garmin v0.3.1: both enroll
with
CAIRN_WORKER_ENROLLMENT_TOKENalone. The placeholder nkey user in nats.conf is only needed for Strava v0.3.0 and earlier, and the Garmin worker no longer needs a static NATS user.
Upgrading to 0.3.0
- Configuration is read only from
CAIRN_-prefixed variables. Unprefixed host variables (URL,USERNAME,PASSWORD,SESSION_SECRET, …) no longer fill Cairn settings; rename any you relied on. - An empty
CAIRN_DATABASE_URL,CAIRN_AUTH_SESSION_SECRETorCAIRN_AUTH_MASTER_ENCRYPTION_KEYnow fails startup. Runcairn config helpto see every variable with its default. - Under NATS auth, upgrade core first: the worker permission template that auth-callout grants now matches the job subjects and JetStream API calls the workers make (see Providers). Workers under NATS without auth are unaffected.
- The Garmin worker image no longer runs as root; see the volume note above.
- Connect-RPC requests are capped at 4 MiB per message and upload bodies are capped (see Request limits).