# openGym: a self-hosted workout tracker with passkey login and Docker

> openGym is an AGPL-3.0 self-hosted gym and body-weight tracker that runs from docker compose, keeps data in a local folder, and signs users in with passkeys. It fits lifters who want plan-based progression without a subscription; it is the wrong tool if you want a managed service or a mobile app store install.

**DuarteSantos8/openGym** — 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.

- Repository: https://github.com/DuarteSantos8/openGym
- Website: https://opengym.duarte-santos.ch
- Stars: 1,585 · Forks: 392
- Language: JavaScript
- License: AGPL-3.0
- Published: 2026-09-10 · Updated: 2026-09-10 · Language: en
- Canonical page: https://hysenlabs.com/projects/duartesantos8-opengym

## What openGym solves, and who ends up running it

Most workout apps keep your training history on someone else's server behind an account you do not control. openGym inverts that. The README's own framing is that it "runs on your box, your data stays in a folder you control, and it's yours to fork." The target user is a lifter who already self-hosts something, is comfortable with Docker Compose, and wants a phone-installable app without a subscription.

The feature set is aimed at structured training rather than casual logging. There is a weekly plan with a routine per weekday over a library the README puts at 1,324 exercises, a guided workout mode that starts today's session and pre-fills weights from last time, supersets that can be planned or created mid-session, warm-up sets that are excluded from estimated 1RM and from the fatigue map, and timed exercises such as planks and loaded carries. Body-weight tracking sits alongside it with a goal line and a chart.

Two groups get less out of it. Anyone who wants a managed service with a support contract is in the wrong place. And anyone who wants to log a workout in ten seconds from a watch has no path here: the README describes a PWA, not a native app, and says nothing about wearable integration.

## How openGym works: Node auth, a JSON store, and a React PWA

The repository splits into api/, frontend/, web/, mcp/, and a docker-compose.yml at the root. The compose file defines a media service and a web service. The media service is a one-time job built on alpine/git that clones github.com/hasaneyldrm/exercises-dataset and copies images and GIFs into ./media; it skips the download if those directories already contain files. The web service is described in the compose comments as passkey auth plus per-user data, Node with no framework, and it can either be pulled as a prebuilt image or built from source.

Authentication is WebAuthn. The .env.example explains that passkeys are bound to an exact hostname and that browsers allow them only over HTTPS, with localhost as the single exception. That is why RP_ID and ORIGIN are the first two variables you set: get them wrong and registration fails before any data is written.

User records live in ./data/db.json. The .env.example tells you to register a passkey profile first, then find your own id under "users"[].id in that file and list it in ADMIN_UIDS. Admins get a dashboard in Settings for disabling accounts, generating invite codes, and viewing workout history. INVITE_ONLY=1 closes signup to anyone without a code. There is also a guest mode that keeps everything in the browser and never touches the server, which the example file notes is still the wrong front door on an instance meant for a known set of people.

## Installing openGym with docker compose and logging a first workout

The README's install instruction is short: copy the environment file, adjust it, and bring the stack up. The defaults in .env.example target a local test on the machine running Docker, so the first run needs no reverse proxy.

```bash
cp .env.example .env
docker compose up -d
```

The defaults set RP_ID=localhost, ORIGIN=http://localhost:8080, WEB_PORT=8080, and RP_NAME=openGym. After the containers start, open http://localhost:8080. The first media download is roughly 140 MB and happens once; later starts print that the media is already present and skip it.

For a real deployment the same file documents the change. Passkeys are bound to the hostname, so you set the values to your domain and let a reverse proxy or tunnel terminate TLS, pointing it at the web container's WEB_PORT.

```bash
RP_ID=gym.example.com
ORIGIN=https://gym.example.com
WEB_PORT=8080
RP_NAME=openGym
```

Once you are in, register a passkey, then build a weekly plan by assigning a routine to each weekday. The guided workout mode picks up today's session, asks for your body weight first, and pre-fills working weights from your last time on that exercise. If you want an admin account, register your profile, open ./data/db.json, copy your id from the users array, and add it to ADMIN_UIDS before restarting.

## Progression rules, warm-up sets, and where the model can surprise you

Progression is the part with the most machinery. You pick one rule per routine and can override it per exercise: linear, Greyskull LP with an AMRAP top set, double jumps and 10 percent resets, double progression through a rep range, or adding time. The README states that missed reps never advance the load, that stalls trigger a deload, and that bodyweight exercises progress in reps instead. Estimated 1RM is computed per exercise from your best eligible set, names which set it used, and will not guess above 12 reps.

The warm-up handling is a deliberate design choice worth understanding before you trust the numbers. Warm-up rows are excluded from estimated 1RM, from progression, and from the fatigue map, but remain visible in the session. A weight change cascades down rows that share a phase, not across the warm-up divide. If you expect a warm-up set to influence your working weight, it will not.

The effort column is optional and off by default. You rate a set as RIR or RPE, and the README is explicit that nothing else reads the value: progression and 1RM are unaffected. That is a clean separation, but it also means the effort data is inert. It is a record, not an input.

## The exercise media licence is separate from the AGPL

The code is AGPL-3.0, but the exercise images and animations are not. The compose file comments state that the dataset's metadata and instruction text are MIT while the images and GIFs are copyright Gym visual, used under the dataset's terms. openGym does not redistribute or relicense them; the media container downloads them from upstream into ./media, and the comments point to NOTICE.md and to Gym visual's terms page.

The practical consequence: running openGym for yourself pulls that media onto your disk under those terms. If you fork the project and ship the media with it, or reuse the images commercially, the compose comments say you need your own licence from Gym visual. This is not legal advice, only what the repository states. Read NOTICE.md and the linked terms before you redistribute anything from ./media.

## Maintenance, upgrades, and the cost of running your own instance

The last push to the repository was on 2026-08-18, and the most recent release, v1.2.7, is dated the same day. Two earlier releases, v1.2.6 and v1.2.5, landed that month as well, so the release cadence is currently frequent. The repository is not archived. That is the extent of what the available facts support; nothing here establishes a long-term support commitment.

Upgrade cost is the standard Docker Compose story: pull or rebuild the web image and restart. The awkward part is the data layer. User records live in ./data/db.json, a single JSON file, and the README and .env.example do not document a migration path, a rollback procedure, or a backup command. If a future release changes that schema, the repository is silent on how you would reverse it. Treat the ./data directory as the thing to back up, and treat upgrades as something to test on a copy first.

The operational cost is real but small: one container plus a one-time media fetch, and a reverse proxy if you want passkeys to work outside localhost. There is no telemetry, which the README lists as a badge, so nothing leaves the box unless you configure it to.

## openGym against a hosted tracker like Strong

Strong is one of the apps openGym lists as an import source, alongside FitNotes and Hevy, which tells you the intended migration path: bring your history over and stop paying for a hosted account. The difference in approach is where the data and the identity live. A hosted tracker keeps your logs on its servers behind its own login and works out of the box on any phone. openGym keeps the logs in ./data/db.json on your machine, signs you in with a passkey bound to your domain, and requires you to run and update the server.

That trade is not close. If you value zero maintenance, the hosted app wins outright, and openGym is the wrong tool. If you value the ability to read your own database file, fork the code under AGPL-3.0, and keep working when a vendor shuts down, openGym is the option that gives you that. The import path is the bridge between the two, and it is one-directional in the sense that the README describes importing into openGym, not exporting back out.

## Conclusion

Adopt openGym if you already run Docker and want your training history in a folder you control, with passkey sign-in and plan-based progression. Do not adopt it if you need a hosted service, an app store install, or a support contract, because the project is a single-maintainer AGPL codebase and the README documents no rollback or backup procedure. Before trusting it with years of logs, verify two things: that your reverse proxy terminates TLS for the exact hostname you put in RP_ID, and that the ./data volume is included in whatever backup you already run.

## FAQ

### How do I install openGym?

Copy .env.example to .env, adjust the values, and run docker compose up -d. The defaults target http://localhost:8080 on the machine running Docker, and the first start downloads roughly 140 MB of exercise media.

### Does openGym need a domain and HTTPS?

For anything other than localhost, yes. The .env.example states that passkeys are bound to the exact hostname and that browsers only allow them over HTTPS, so you set RP_ID and ORIGIN to your domain and put the web container behind a reverse proxy or tunnel that terminates TLS.

### Where does openGym store my workout data?

Per-user records live in ./data/db.json on the server you run, and the README describes that folder as the one you control. Guest mode is the exception: it keeps everything in the browser and never touches the server.

### Are the exercise images and GIFs covered by the AGPL licence?

No. The docker-compose.yml comments state that the dataset's metadata and instruction text are MIT, while the images and GIFs are copyright Gym visual and used under that dataset's terms. openGym does not redistribute or relicense them; they are downloaded from upstream, and reusing them yourself needs your own licence.

### Can I import my history from Strong, FitNotes or Hevy?

The project description lists import from FitNotes, Strong and Hevy as a feature. The README does not document the import file format or any limits on what transfers.

## Sources

- [DuarteSantos8/openGym on GitHub](https://github.com/DuarteSantos8/openGym)
- [License: AGPL-3.0](https://github.com/DuarteSantos8/openGym/blob/main/LICENSE)
- [Project website](https://opengym.duarte-santos.ch)
- [README](https://github.com/DuarteSantos8/openGym/blob/main/README.md)
- [Releases](https://github.com/DuarteSantos8/openGym/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/duartesantos8-opengym
