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.
docker-compose.yml (sketch)
services:
core:
image: ghcr.io/johnnycube/cairn-core:latest
env_file: .env
ports: ["8090:8090"]
depends_on: [postgres, nats]
worker-strava:
image: ghcr.io/johnnycube/cairn-provider-strava:latest
env_file: .env
worker-garmin:
image: ghcr.io/johnnycube/cairn-provider-garmin:latest
env_file: .env
postgres:
image: timescale/timescaledb-ha:pg17-ts2.27
nats: { image: nats:2-alpine, command: ["-js"] }
minio: { image: minio/minio, command: ["server", "/data"] }
Put a TLS-terminating reverse proxy (Caddy, nginx, Traefik) in front of core. See Configuration for the full variable list.