Self-host everything.
Own your data.
Cadiv runs entirely on your own hardware. No SaaS subscription, no data
harvesting, no third-party analytics. Pull the images, add your keys,
and docker compose up.
Containers
2
Setup Time
~10 min
Min RAM
512 MB
Prerequisites
What you need
A Server
Any Linux box, VPS, Raspberry Pi, NAS, or even your Mac. 512 MB RAM minimum, 1 GB recommended.
Docker & Compose
Docker Engine 20+ and Docker Compose v2. That's the only system dependency.
Strava API App
A free Strava API application for activity import. Takes 2 minutes to create. Optional — connect after login from Settings.
Whoop API App (optional)
Optional Whoop developer app for recovery and strain data. Free to create.
Guide
Step-by-step setup
From zero to riding in about 10 minutes.
Create a Strava API Application
Strava is used for activity import and sync. Create a free API app to connect your rides after logging in.
Go to strava.com/settings/api and log in with your Strava account.
Create a new application with these settings:
| Application Name | Cadiv (or anything you like) |
| Category | Training |
| Club | (leave blank) |
| Website | Your domain or http://localhost:4321 |
| Authorization Callback Domain | localhost or your domain |
Note your Client ID and Client Secret — you'll need them in Step 3.
Create a Whoop Developer App
OptionalIf you use a Whoop band and want recovery, strain, and workout data, set up a developer app. Skip this if you only use Strava.
Go to developer-dashboard.whoop.com and sign in.
Create a new application. Set the redirect URI to:
Replace localhost with your domain
if deploying remotely.
Enable these scopes:
Note your Client ID and Client Secret.
Create your config files
Create a new directory and two files. No source code needed — Docker pulls the pre-built images.
Create a directory
$ mkdir cadiv && cd cadiv
Create docker-compose.yml
Paste this into docker-compose.yml:
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes: [ cadiv-pgdata:/var/lib/postgresql/data ]
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U cadiv" ]
cadiv:
image: lukevinskywynn/cadiv:latest
depends_on: { postgres: { condition: service_healthy } }
env_file: .env
ports: [ "4000:4000", "4321:4321" ]
volumes:
cadiv-pgdata:
The full docker-compose.yml with all environment variables is available on Docker Hub.
Create your .env file
Create a .env file in the same directory with your secrets:
# Your login credentials
CADIV_USERNAME=admin
CADIV_PASSWORD=your-strong-password
# PostgreSQL
POSTGRES_PASSWORD=your-strong-database-password
# Session secret (generate: openssl rand -hex 32)
SESSION_SECRET=your-64-char-hex-string
# Strava (optional, from Step 1)
STRAVA_CLIENT_ID=12345
STRAVA_CLIENT_SECRET=abc123def456...
# Whoop (optional, from Step 2)
WHOOP_CLIENT_ID=your-whoop-id
WHOOP_CLIENT_SECRET=your-whoop-secret
Tip: Generate a secure session secret
Pull & Start
One command to pull the image and start everything.
$ docker compose up -d
[+] Pulling 2/2
[+] Running 2/2
✔ Container cadiv-postgres Started
✔ Container cadiv Started
The first run downloads the pre-built image from Docker Hub. Subsequent starts are near-instant.
What happens on startup
prisma migrate deploy to create/update tables CADIV_USERNAME / CADIV_PASSWORD Sign In & Ride
Open your browser and sign in with your credentials.
Open http://localhost:4321 in your browser.
Sign in with the username and
password you set in your
.env file.
Go to Settings → Connected Accounts → Connect Strava to import your ride history.
(Optional) Connect Whoop for recovery and strain data.
Architecture
What's in the box
PostgreSQL 16
cadiv-postgres
All your data lives here — activities, coaching plans, user profiles, notifications, sync cursors. Persistent volume means your data survives container restarts and upgrades.
Port: 5432 · Volume: cadiv-pgdata
Cadiv
lukevinskywynn/cadiv
A single container running the full stack: Express API server with Prisma ORM (port 4000) and Astro + React web client (port 4321). Handles OAuth, data import, training insights, coaching engine, notifications, and background sync.
Ports: 4000 (API) + 4321 (Web)
Advanced
Custom domain & HTTPS
Deploying to a VPS with a custom domain? Here's how to update the configuration.
1. Update redirect URLs
Replace localhost with your domain in .env:
APP_ORIGIN=https://cadiv.yourdomain.com
STRAVA_REDIRECT_URI=https://cadiv.yourdomain.com/api/auth/strava/callback
STRAVA_SUCCESS_REDIRECT=https://cadiv.yourdomain.com/app/dash
STRAVA_FAILURE_REDIRECT=https://cadiv.yourdomain.com/?auth=error
2. Update Strava callback domain
Go back to strava.com/settings/api and update the Authorization Callback Domain to your
domain (e.g., cadiv.yourdomain.com).
3. Add a reverse proxy
Use Nginx, Caddy, or Traefik to terminate TLS and proxy to the containers. Example with Caddy:
cadiv.yourdomain.com {
handle /api/* {
reverse_proxy localhost:4000
}
handle {
reverse_proxy localhost:4321
}
}
Caddy automatically provisions Let's Encrypt certificates. Zero HTTPS configuration needed.
Operations
Common commands
docker compose up -d
Start all containers in background
docker compose down
Stop all containers
docker compose logs -f cadiv
Follow Cadiv logs
docker compose pull && docker compose up -d
Update to latest version
docker compose exec postgres pg_dump -U cadiv cadiv > backup.sql
Backup database
docker compose exec -T postgres psql -U cadiv cadiv < backup.sql
Restore database
docker compose restart cadiv
Restart Cadiv
Reference
Environment variables
| Variable | Required | Description |
|---|---|---|
| DATABASE_URL | No | Full PostgreSQL connection string. If set, uses this instead of the bundled Postgres container |
| POSTGRES_PASSWORD | Yes | Password for the bundled PostgreSQL container. Not needed if using DATABASE_URL |
| SESSION_SECRET | Yes | Secret for signing session cookies. Use openssl rand -hex 32 |
| STRAVA_CLIENT_ID | Yes | Your Strava API application Client ID |
| STRAVA_CLIENT_SECRET | Yes | Your Strava API application Client Secret |
| STRAVA_REDIRECT_URI | No | OAuth callback URL. Default: http://localhost:4000/api/auth/strava/callback |
| STRAVA_SUCCESS_REDIRECT | No | URL after successful auth. Default: http://localhost:4321/app/dash |
| STRAVA_FAILURE_REDIRECT | No | URL after failed auth. Default: http://localhost:4321/?auth=error |
| WHOOP_CLIENT_ID | No | Whoop developer app Client ID (optional) |
| WHOOP_CLIENT_SECRET | No | Whoop developer app Client Secret (optional) |
| WHOOP_REDIRECT_URI | No | Whoop OAuth callback URL (optional) |
| APP_ORIGIN | No | Frontend URL. Default: http://localhost:4321 |
| CLIENT_PORT | No | Host port for web UI. Default: 4321 |
| SERVER_PORT | No | Host port for API. Default: 4000 |
| POSTGRES_PORT | No | Host port for PostgreSQL. Default: 5432 |
Mobile
Connecting the mobile app
The Cadiv mobile app (React Native / Expo) can point to your self-hosted server for a fully private mobile experience.
1. Download the Cadiv app
Install the Cadiv mobile app from the App Store or Google Play. The same app works with any Cadiv server — no custom builds required.
2. Connect to your server
When you open the app for the first time, you'll see the Connect to Server screen. Enter your server URL (e.g., https://cadiv.yourdomain.com or http://192.168.1.50:4000).
The app verifies the connection by fetching /api/config from your server. Once verified, it stores the URL securely and proceeds
to login — using your server's own Strava app credentials.
Zero configuration
The server URL is configured in-app — not hardcoded. You can change it anytime from Settings → Server.
3. Authenticate via Strava
Tap Connect with Strava. The app opens the Strava OAuth flow against your server. Your server exchanges the auth code, creates a JWT, and redirects back to the app. All authentication is handled by your self-hosted server — nothing touches any external service beyond Strava's OAuth.
Your data.
Your server.
Your edge.
No subscriptions. No data harvesting. Just you, your bike, and your own server.