Self-hosted gym & body-weight tracker — plan routines, log workouts (supersets, warm-ups, cardio), see which muscles are trained, fatigued or detrained, import from FitNotes/Strong/Hevy, passkey login. Your data, your server.
See the code
A self-hosted gym & body-weight tracker you actually own.
Plan your week, run guided workouts, track every set and your body weight over time —
on your phone, synced across devices, behind your own passkey login.
No account on someone else's server, no subscription, no ads. Just docker compose up.
![]() Home — today's workout & weight | ![]() Guided workout — animated demos & sets | ![]() Stats — heatmap, charts & PRs |
Screenshots, docs and the APK download live on the site.
Want to poke at it first? The in-browser
demo is the real app with example data — no account, nothing to install.
Most workout apps lock your data behind a login on their servers, nag you to upgrade, or disappear when the startup does. openGym is the opposite: it runs on your box, your data stays in a folder you control, and it's yours to fork. It still feels modern — installable as a home-screen app, passkey sign-in, offline support, sync across your phone and laptop.
You need Docker with Compose.
git clone https://github.com/DuarteSantos8/openGym
cd openGym
cp .env.example .env
docker compose pull # grab prebuilt images (amd64 + arm64) — skip to build from source instead
docker compose up -d
Open http://localhost:8080, tap Create profile, and you're in. First launch downloads the exercise media (~140 MB) once.
About that media: it reaches openGym through hasaneyldrm/exercises-dataset, which redistributes ExerciseDB v1 — its metadata and instruction text are MIT, but the images and animations are third-party content under neither that MIT license nor openGym's AGPL, and their ownership is currently disputed between Gym visual and ExerciseDB. openGym ships none of it: your instance downloads it from upstream. Reusing it yourself, commercially or not, means clearing it with the rights holder — see NOTICE.md. The prebuilt images are published twice, from the same tag:
registry.gitlab.com/duartesantos8/opengym/{api,web}(whatdocker-compose.ymlpulls) andghcr.io/duartesantos8/opengym-{api,web}on GitHub — swap theimage:lines if you prefer GHCR. Prefer building the images yourself instead of pulling from a registry? Drop thepullstep and rundocker compose up -d --build— you don't need Node or a build step locally either way.
Want it reachable from your phone over the internet with passkeys? You'll need an HTTPS domain — a two-line change in
.env. See docs/SELF_HOSTING.md.
The same codebase also builds a standalone mobile app (Capacitor): no account, no sync, no backend — everything stays on the phone, with native workout-day reminders and share-sheet backups. Self-hosting gets you multi-device sync and profiles for friends & family; the mobile app is the install-and-done flavor.
.sha256 — and sideload it; openGym is deliberately not on the Play
Store. Or build it yourself: docs/MOBILE.md.┌─────────────┐ ┌──────────────────────────────┐
│ Your phone │──HTTPS─▶│ web (nginx) │
│ / laptop │ │ ├─ serves the built app │
└─────────────┘ │ └─ proxies /api ──────────┐│
└──────────────────────────────┘│
▼
┌──────────────────────────┐
│ api (Node + WebAuthn) │
│ └─ ./data (JSON files) │
└──────────────────────────┘
@simplewebauthn/server for passkeys, web-push for notifications), storing everything as plain JSON files under ./data/api to the backend so it's all on one origin (passkeys require this)The full HTTP API is documented as an OpenAPI spec in api/openapi.yaml — browsable at opengym.duarte-santos.ch/api.html.
Lives in ./data on your host: db.json (profiles + public passkeys), state-<user>.json
(each user's plan, workouts, body weight, settings), audit.log (the admin activity log — sign-ins
and admin actions, no IP addresses unless you ask for them) and secret (the session-cookie key).
Back up ./data and you've backed up everything. Passkey private keys never touch the
server — they stay in your phone's secure hardware / your password manager.
All via .env (see .env.example):
| Variable | What it is | Default |
|---|---|---|
RP_ID | Hostname passkeys are bound to | localhost |
ORIGIN | Full URL the app is served from | http://localhost:8080 |
WEB_PORT | Host port for the web UI | 8080 |
NGINX_PORT | Port the web container listens on, inside the container | 80 |
BACKEND | Name of the API service that /api is proxied to — change it if yours isn't called api | api |
PORT | Port the API listens on; the web container proxies to the same value | 3000 |
RP_NAME | Name shown in the passkey prompt | openGym |
SESSION_DAYS | How long a sign-in lasts, in days | 90 |
ADMIN_UIDS | User ids that get the admin dashboard (comma-separated) | (none) |
INVITE_ONLY | Require an invite code to create a profile | (off) |
ALLOW_GUEST | Offer "Continue without account" — set 0 to require a profile | (on) |
AUDIT_LOG | Record sign-ins and admin actions — set 0 to record nothing | (on) |
AUDIT_MAX | Events kept in the activity log; 0 for no limit | 5000 |
AUDIT_DAYS | Days kept in the activity log; 0 to keep until AUDIT_MAX | 90 |
AUDIT_IP | Record the caller's address: off, net (network only) or full | off |
VAPID_SUBJECT | Contact URL sent with push notifications | your ORIGIN |
API_TARGET | Which API image to build: default (no AI runtime — API-key providers still work) or coach (adds the Claude Agent SDK + Codex CLI) | default |
COACH_DISABLED | Set to 1 to force the AI Coach off instance-wide, whatever the admin toggled | (unset) |
Push notification keys are generated on first run and saved to ./data/vapid.json — nothing to set.
DATA_DIR is pinned to /data by docker-compose.yml and mapped to ./data on the host; change the
host side of that volume, not the variable.
The plan lives in ROADMAP.md, and the GitHub milestones hold the issues. A release every two weeks, each one small and themed: the promised items, editing finished workouts, the session queue, programmes and phases, the progression engine, cardio — then v1.4.0, the foundation: storage moves to a database and search is rebuilt, the one compatibility break — then accounts (password and OIDC login, trainer role, MCP write), the iOS app, the Android and health items, and what all of that unlocks (pictures for custom exercises, catalogue work, skins, social). Ideas and pull requests welcome.
React 19 + Vite (React Router, Zustand) · Node (no framework) · nginx · Docker Compose ·
WebAuthn · exercise data from hasaneyldrm/exercises-dataset
(MIT metadata and instructions; media © Gym visual — see License).
No database server, no cloud dependencies — the frontend builds inside Docker, so self-hosting
stays a one-command docker compose up.
The training logic — progression rules, 1RM estimation, how a logged session is read back —
lives in pure functions under frontend/src/lib/ with tests next to them: npm test in
frontend/. Vitest is a dev dependency; the app itself ships no runtime dependencies beyond
React, the router and Zustand.
The optional AI Coach (api/coach/) is built the same way round: a by-name allowlist decides
what may leave the server, and a closed-list validator decides what may come back — the model
can touch routines and the weekly schedule, nothing else, and every change is applied on the
client only after you approve it. The core of it — api/coach/core/ — has no Node dependency,
so the phone app runs the same validator the server does. The in-container AI runtimes live in a
separate Docker build target; the API-key providers need none. See docs/AI_COACH.md.
The same pure helpers power an optional MCP server (mcp/) that lets an LLM client like
Claude Desktop read your data over stdio — see mcp/README.md. Opt-in, not
in the Docker build.
question and an idea idea, and it gets treated as one rather than as agreed-on work. Use
an issue over the Discord for anything the next person should be able to find by searching.RP_ID/ORIGIN mismatch — check
docs/SELF_HOSTING.md before opening an issue.main here; new work, please, as a pull request.GitHub is home; GitLab is a mirror.
github.com/DuarteSantos8/openGymwas offline from 2026-08-19 to 2026-09-10 while the account was suspended, and the project lived on GitLab in the meantime. It is back, and gitlab.com/DuarteSantos8/opengym is now kept in sync by a GitHub Actions workflow on every push tomainand everyv*tag — nothing is pushed or merged there by hand. The mirror stays because its CI builds the release artefacts: the signed APK, the multi-arch images (GitLab registry, mirrored to GHCR) and the SBOMs. (gitea.com/DuarteSantos/openGym is a plain mirror.) In CHANGELOG.md,!NNis a GitLab merge request from those weeks;#NNrefers to whichever tracker the report came through.
Issues and PRs welcome — see CONTRIBUTING.md. Good first issues: more starter plans, exercise-data languages, import from other trackers. A ⭐ helps more people find it.
openGym is free and stays free: AGPL, no subscription, no paid tier, nothing held back for sponsors. If it replaced a paid tracker for you and you want to chip in, there's a coffee button below (and a badge at the top) — a star, a bug report or a merge request is worth just as much.
openGym's own code is GNU AGPL v3.0 — free and open source. You can self-host, use, modify and share it; if you run a modified version as a network service, you must offer that version's source under the same license. Nobody can turn openGym into a closed, proprietary product.
Third-party content is not, and openGym cannot sublicense it. The exercise metadata and instruction text originate from ExerciseDB v1 and reach openGym through hasaneyldrm/exercises-dataset under the MIT license. The exercise images and animations are third-party content covered by neither that license nor the AGPL, and their ownership is currently unresolved — the upstream dataset attributes them to Gym visual under a non-transferable permission, while ExerciseDB/AscendAPI claims to be their creator and owner. A clarification has been requested. openGym does not redistribute them (your instance fetches them at first run) and does not relicense them. To reuse that media yourself, clear it with the rights holder first.
Full third-party notices, including the body-diagram geometry: NOTICE.md.
(top 30 of 33)
JavaScript
89.2%
HTML
6.3%
CSS
3.8%
Self-hosted gym & body-weight tracker — plan routines, log workouts (supersets, warm-ups, cardio), see which muscles are trained, fatigued or detrained, import from FitNotes/Strong/Hevy, passkey login. Your data, your server.
See the code
A self-hosted gym & body-weight tracker you actually own.
Plan your week, run guided workouts, track every set and your body weight over time —
on your phone, synced across devices, behind your own passkey login.
No account on someone else's server, no subscription, no ads. Just docker compose up.
![]() Home — today's workout & weight | ![]() Guided workout — animated demos & sets | ![]() Stats — heatmap, charts & PRs |
Screenshots, docs and the APK download live on the site.
Want to poke at it first? The in-browser
demo is the real app with example data — no account, nothing to install.
Most workout apps lock your data behind a login on their servers, nag you to upgrade, or disappear when the startup does. openGym is the opposite: it runs on your box, your data stays in a folder you control, and it's yours to fork. It still feels modern — installable as a home-screen app, passkey sign-in, offline support, sync across your phone and laptop.
You need Docker with Compose.
git clone https://github.com/DuarteSantos8/openGym
cd openGym
cp .env.example .env
docker compose pull # grab prebuilt images (amd64 + arm64) — skip to build from source instead
docker compose up -d
Open http://localhost:8080, tap Create profile, and you're in. First launch downloads the exercise media (~140 MB) once.
About that media: it reaches openGym through hasaneyldrm/exercises-dataset, which redistributes ExerciseDB v1 — its metadata and instruction text are MIT, but the images and animations are third-party content under neither that MIT license nor openGym's AGPL, and their ownership is currently disputed between Gym visual and ExerciseDB. openGym ships none of it: your instance downloads it from upstream. Reusing it yourself, commercially or not, means clearing it with the rights holder — see NOTICE.md. The prebuilt images are published twice, from the same tag:
registry.gitlab.com/duartesantos8/opengym/{api,web}(whatdocker-compose.ymlpulls) andghcr.io/duartesantos8/opengym-{api,web}on GitHub — swap theimage:lines if you prefer GHCR. Prefer building the images yourself instead of pulling from a registry? Drop thepullstep and rundocker compose up -d --build— you don't need Node or a build step locally either way.
Want it reachable from your phone over the internet with passkeys? You'll need an HTTPS domain — a two-line change in
.env. See docs/SELF_HOSTING.md.
The same codebase also builds a standalone mobile app (Capacitor): no account, no sync, no backend — everything stays on the phone, with native workout-day reminders and share-sheet backups. Self-hosting gets you multi-device sync and profiles for friends & family; the mobile app is the install-and-done flavor.
.sha256 — and sideload it; openGym is deliberately not on the Play
Store. Or build it yourself: docs/MOBILE.md.┌─────────────┐ ┌──────────────────────────────┐
│ Your phone │──HTTPS─▶│ web (nginx) │
│ / laptop │ │ ├─ serves the built app │
└─────────────┘ │ └─ proxies /api ──────────┐│
└──────────────────────────────┘│
▼
┌──────────────────────────┐
│ api (Node + WebAuthn) │
│ └─ ./data (JSON files) │
└──────────────────────────┘
@simplewebauthn/server for passkeys, web-push for notifications), storing everything as plain JSON files under ./data/api to the backend so it's all on one origin (passkeys require this)The full HTTP API is documented as an OpenAPI spec in api/openapi.yaml — browsable at opengym.duarte-santos.ch/api.html.
Lives in ./data on your host: db.json (profiles + public passkeys), state-<user>.json
(each user's plan, workouts, body weight, settings), audit.log (the admin activity log — sign-ins
and admin actions, no IP addresses unless you ask for them) and secret (the session-cookie key).
Back up ./data and you've backed up everything. Passkey private keys never touch the
server — they stay in your phone's secure hardware / your password manager.
All via .env (see .env.example):
| Variable | What it is | Default |
|---|---|---|
RP_ID | Hostname passkeys are bound to | localhost |
ORIGIN | Full URL the app is served from | http://localhost:8080 |
WEB_PORT | Host port for the web UI | 8080 |
NGINX_PORT | Port the web container listens on, inside the container | 80 |
BACKEND | Name of the API service that /api is proxied to — change it if yours isn't called api | api |
PORT | Port the API listens on; the web container proxies to the same value | 3000 |
RP_NAME | Name shown in the passkey prompt | openGym |
SESSION_DAYS | How long a sign-in lasts, in days | 90 |
ADMIN_UIDS | User ids that get the admin dashboard (comma-separated) | (none) |
INVITE_ONLY | Require an invite code to create a profile | (off) |
ALLOW_GUEST | Offer "Continue without account" — set 0 to require a profile | (on) |
AUDIT_LOG | Record sign-ins and admin actions — set 0 to record nothing | (on) |
AUDIT_MAX | Events kept in the activity log; 0 for no limit | 5000 |
AUDIT_DAYS | Days kept in the activity log; 0 to keep until AUDIT_MAX | 90 |
AUDIT_IP | Record the caller's address: off, net (network only) or full | off |
VAPID_SUBJECT | Contact URL sent with push notifications | your ORIGIN |
API_TARGET | Which API image to build: default (no AI runtime — API-key providers still work) or coach (adds the Claude Agent SDK + Codex CLI) | default |
COACH_DISABLED | Set to 1 to force the AI Coach off instance-wide, whatever the admin toggled | (unset) |
Push notification keys are generated on first run and saved to ./data/vapid.json — nothing to set.
DATA_DIR is pinned to /data by docker-compose.yml and mapped to ./data on the host; change the
host side of that volume, not the variable.
The plan lives in ROADMAP.md, and the GitHub milestones hold the issues. A release every two weeks, each one small and themed: the promised items, editing finished workouts, the session queue, programmes and phases, the progression engine, cardio — then v1.4.0, the foundation: storage moves to a database and search is rebuilt, the one compatibility break — then accounts (password and OIDC login, trainer role, MCP write), the iOS app, the Android and health items, and what all of that unlocks (pictures for custom exercises, catalogue work, skins, social). Ideas and pull requests welcome.
React 19 + Vite (React Router, Zustand) · Node (no framework) · nginx · Docker Compose ·
WebAuthn · exercise data from hasaneyldrm/exercises-dataset
(MIT metadata and instructions; media © Gym visual — see License).
No database server, no cloud dependencies — the frontend builds inside Docker, so self-hosting
stays a one-command docker compose up.
The training logic — progression rules, 1RM estimation, how a logged session is read back —
lives in pure functions under frontend/src/lib/ with tests next to them: npm test in
frontend/. Vitest is a dev dependency; the app itself ships no runtime dependencies beyond
React, the router and Zustand.
The optional AI Coach (api/coach/) is built the same way round: a by-name allowlist decides
what may leave the server, and a closed-list validator decides what may come back — the model
can touch routines and the weekly schedule, nothing else, and every change is applied on the
client only after you approve it. The core of it — api/coach/core/ — has no Node dependency,
so the phone app runs the same validator the server does. The in-container AI runtimes live in a
separate Docker build target; the API-key providers need none. See docs/AI_COACH.md.
The same pure helpers power an optional MCP server (mcp/) that lets an LLM client like
Claude Desktop read your data over stdio — see mcp/README.md. Opt-in, not
in the Docker build.
question and an idea idea, and it gets treated as one rather than as agreed-on work. Use
an issue over the Discord for anything the next person should be able to find by searching.RP_ID/ORIGIN mismatch — check
docs/SELF_HOSTING.md before opening an issue.main here; new work, please, as a pull request.GitHub is home; GitLab is a mirror.
github.com/DuarteSantos8/openGymwas offline from 2026-08-19 to 2026-09-10 while the account was suspended, and the project lived on GitLab in the meantime. It is back, and gitlab.com/DuarteSantos8/opengym is now kept in sync by a GitHub Actions workflow on every push tomainand everyv*tag — nothing is pushed or merged there by hand. The mirror stays because its CI builds the release artefacts: the signed APK, the multi-arch images (GitLab registry, mirrored to GHCR) and the SBOMs. (gitea.com/DuarteSantos/openGym is a plain mirror.) In CHANGELOG.md,!NNis a GitLab merge request from those weeks;#NNrefers to whichever tracker the report came through.
Issues and PRs welcome — see CONTRIBUTING.md. Good first issues: more starter plans, exercise-data languages, import from other trackers. A ⭐ helps more people find it.
openGym is free and stays free: AGPL, no subscription, no paid tier, nothing held back for sponsors. If it replaced a paid tracker for you and you want to chip in, there's a coffee button below (and a badge at the top) — a star, a bug report or a merge request is worth just as much.
openGym's own code is GNU AGPL v3.0 — free and open source. You can self-host, use, modify and share it; if you run a modified version as a network service, you must offer that version's source under the same license. Nobody can turn openGym into a closed, proprietary product.
Third-party content is not, and openGym cannot sublicense it. The exercise metadata and instruction text originate from ExerciseDB v1 and reach openGym through hasaneyldrm/exercises-dataset under the MIT license. The exercise images and animations are third-party content covered by neither that license nor the AGPL, and their ownership is currently unresolved — the upstream dataset attributes them to Gym visual under a non-transferable permission, while ExerciseDB/AscendAPI claims to be their creator and owner. A clarification has been requested. openGym does not redistribute them (your instance fetches them at first run) and does not relicense them. To reuse that media yourself, clear it with the rights holder first.
Full third-party notices, including the body-diagram geometry: NOTICE.md.
(top 30 of 33)
JavaScript
89.2%
HTML
6.3%
CSS
3.8%