An open-source, self-hosted running app: log your runs, sync from your watch, and organise your run club (including the coffee order).
Your runs are yours: they're private unless you choose to share them, and clubs only ever see what you send them. Run it for yourself, or for friends and family who trust you with their data. It's inspired by openGym.
Try it: openrun-demo.fly.dev signs you in as Sam, a runner in a run club. It resets every night.
Status: early development. Accounts, the run log, stats, privacy controls, run clubs and watch sync work. v0.1 is out (release notes). See ROADMAP.md.
![]() |
![]() |
![]() |
![]() |
| Status | |
|---|---|
| Accounts with passkeys or password, first user becomes admin, invite-only mode | ✅ |
| Installable PWA (works offline from cache, update prompt) | ✅ |
| Single Docker image with SQLite, migrations applied on start | ✅ |
| Run log: FIT/GPX import, maps, splits, PBs, shoes, stats, export | ✅ |
| Privacy: private by default, sharing settings, privacy zones | ✅ |
| Run clubs: repeating runs, RSVPs, coffee orders, weather, reminders | ✅ |
| Watch sync via intervals.icu (Garmin, Coros, Polar, Suunto, Wahoo) | ✅ |
curl -O https://raw.githubusercontent.com/stretch0/openRun/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/stretch0/openRun/main/.env.exampleSet BETTER_AUTH_SECRET (openssl rand -base64 32) and ORIGIN (the URL people will use) in .env, then:
docker compose up -dOpen the URL. The first account you create becomes the admin. Everything is stored in ./data (see Backups).
No server of your own? docs/FLY.md runs it on Fly.io for a few dollars a month.
Passkeys need HTTPS (or localhost), so put openRun behind a reverse proxy such as Caddy or Traefik for anything beyond local use. When you do, set TRUST_PROXY=true. All settings are listed in .env.example. To run a public demo with sample data instead, see docs/DEMO.md.
Back up first, then:
docker compose pull
docker compose up -dDatabase changes are applied when openRun starts; docker compose logs openrun shows how many. There's no way back to an older version except restoring the backup you made before upgrading. latest follows every change on main; to upgrade only when you choose, pin a release in docker-compose.yml (for example ghcr.io/stretch0/openrun:0.1).
Everything lives in two places: the ./data folder and your .env. Keep .env safe as well, because BETTER_AUTH_SECRET signs everyone's sessions and encrypts their intervals.icu keys. A database restored without it works, but everyone has to sign in and paste their intervals.icu key again.
./data holds:
| Path | What it is |
|---|---|
openrun.db (plus -wal and -shm) |
The database: accounts, runs, clubs, settings |
files/ |
Original FIT and GPX files |
vapid.json |
Push notification keys (losing them means everyone turns pushes on again) |
backups/ |
Copies made by backup.js |
The simplest backup is to stop openRun, copy the folder and start it again:
docker compose stop
tar czf openrun-$(date +%F).tar.gz data .env
docker compose startTo back up without stopping, copy the database with the built-in command first (copying openrun.db itself while openRun runs can miss recent changes), then archive the folder:
docker compose exec openrun node backup.js
tar czf openrun-$(date +%F).tar.gz --exclude='data/openrun.db*' data .envThe archive then holds the database copy from data/backups/. Delete old copies from data/backups/ now and then.
To restore, stop openRun, unpack the archive, and if it came from backup.js, put that copy in place as openrun.db:
docker compose stop
tar xzf openrun-2026-10-30.tar.gz
rm -f data/openrun.db-wal data/openrun.db-shm
cp data/backups/openrun-2026-10-30T0915.db data/openrun.db # only for backup.js copies
docker compose startA backup from an older version is upgraded when openRun starts.
There's no password reset by email, and admins can't reset passwords from inside the app. Whoever runs the server can:
docker compose exec openrun node reset-password.js someone@example.comIt prints a temporary password and signs that person out everywhere. Send it to them privately; they can change it in Settings.
- Runs are visible only to their owner unless they choose to share them.
- Shared runs show the route only if the owner allows it, with their privacy zones cut out.
- Admins manage invites and settings; they can't see other people's runs.
- Whoever runs the server can read what's stored on it, as with any server. Set
INSTANCE_CONTACTso members know who that is, and only host data for people who trust you.
The full design, including plans for clubs that connect separate instances, is in docs/PRIVACY.md.
You need Node 22 or later.
npm install
npm run devThe API runs on http://localhost:3000 and the PWA on http://localhost:4200, which proxies /api to the API. The development database is created at ./data/openrun.db.
If something else already uses port 3000, pick another for the API; the PWA follows it:
PORT=3005 npm run dev| Command | What it does |
|---|---|
npm run dev |
API and web app in watch mode |
npm run lint |
ESLint and type checks |
npm test |
Unit tests (Vitest) |
npm run e2e |
Playwright tests against the production build |
npm run db:generate -- --name=<description> |
Generate a migration after changing libs/db/src/schema |
npx nx affected -t lint typecheck test build |
Only what your change affects |
The repo is an Nx monorepo. See docs/ARCHITECTURE.md for how it fits together.
Want to help? See CONTRIBUTING.md.
AGPL-3.0-or-later. If you run a modified openRun for other people, you must share your changes.



