openGym is a free, AGPL-licensed workout and body-weight tracker that runs on your own machine instead of a company's cloud: you plan routines, log sets, and it syncs between your phone and laptop behind a passkey login. It added 1,433 GitHub stars today, taking it past 4,300, and with Docker already installed the first local run takes about five minutes.
The pitch is aimed squarely at people tired of Strong or Hevy subscriptions. Your history sits in a ./data folder of JSON files you can back up with one tar command, and the app still behaves like a modern one: installable on the home screen, works offline, signs you in with Face ID or a fingerprint. The catch is the passkey part, which needs HTTPS before your phone will cooperate. Most of this guide is about getting that right.
RelatedOpenStock Setup: Self-Host a Free Stock Tracker in 20 Minutes
- Two containers and a folder: nginx serves the React app and proxies
/apito a plain Node API that stores everything as JSON under./data. - Current version: v1.3.9, released 28 September 2026, with prebuilt images for amd64 and arm64 and a 16 MB signed Android APK.
- Brings your history with it: imports from FitNotes, Strong, Hevy and Apple Health weight exports, and exports everything as one JSON file.
- No AI unless you ask: the AI coach and the MCP server are opt-in and off in a default install.
The exact steps, start to finish
- Check your prerequisites. You need Git and Docker with the Compose plugin. Node is not required on the host.
git --version docker --version docker compose version - Clone the repo and create your config.
git clone https://github.com/DuarteSantos8/openGym cd openGym cp .env.example .env - Pull the prebuilt images and start the stack. The first start also downloads about 140 MB of exercise images and animations, once.
docker compose pull docker compose up -d - Confirm it is healthy. The health endpoint should answer with
{"ok":true,...}.docker compose ps curl http://localhost:8080/api/health - Create your profile. Open
http://localhost:8080on the same machine, tap Create profile and register a passkey. No account anywhere else is needed. - Put it on your own HTTPS domain for your phone. Route a hostname such as
gym.example.comto port 8080 through Cloudflare Tunnel, Caddy, Traefik or nginx, then edit.env:RP_ID=gym.example.com ORIGIN=https://gym.example.com WEB_PORT=8080 RP_NAME=openGym - Apply the new settings. Use
up -d, notrestart, so the containers re-read.env.docker compose up -d - Install it on your phone and train. Visit
https://gym.example.com, create your profile, add it to the home screen (iOS: Share, then Add to Home Screen; Android: the menu, then Add to Home screen), load one of the four starter plans and start today's workout.
./data and you have backed up the whole instance.Try the openGym demo before installing anything
The project hosts an in-browser demo that is the real app loaded with example data. Spend two minutes there first. Open today's workout, tick off a set and watch the rest timer start, then flip to the stats screen for the year-long heatmap and the muscle map, which shows where your volume went, what is still recovering and what has gone untrained. If the guided-workout screen does not click for you, nothing about self-hosting will change that.
The feature list is long for a project created in July. There are 1,324 exercises with animated demos, filterable by the equipment you own, plus supersets, drop sets, warm-ups, timed holds and cardio. Progression rules include linear, Greyskull LP and double progression, and each suggested weight explains why it is that number. A plate calculator works from the plates you actually have. The app ships in 17 languages, including right-to-left Arabic.
Running openGym with Docker Compose on Windows, macOS and Linux
The commands are identical on all three because everything happens inside containers. On Windows and macOS that means Docker Desktop running in the background; on Linux, Docker Engine with the Compose plugin. The README's quick start is five lines:
git clone https://github.com/DuarteSantos8/openGym
cd openGym
cp .env.example .env
docker compose pull # prebuilt images, amd64 + arm64 (skip this to build from source)
docker compose up -d
On Windows, run those from Git Bash or WSL so cp works as written. In plain cmd.exe the equivalent copy is copy .env.example .env. In our clone the compose file pulled from ghcr.io/duartesantos8/opengym-api and opengym-web, and the same tags are published to GitLab's registry at registry.gitlab.com/duartesantos8/opengym if you prefer that mirror. If you want to build from source instead, skip the pull and run docker compose up -d --build.
The default .env is already set for local testing: RP_ID=localhost, ORIGIN=http://localhost:8080, WEB_PORT=8080. Logs come from docker compose logs -f and docker compose down stops everything. Updating later is git pull && docker compose pull && docker compose up -d, and your ./data folder is left alone.
For this guide's video we drove the hosted demo rather than a local stack, because the test laptop was short on free memory for Docker Desktop alongside our recording tools. The commands above are copied from the project's README and self-hosting guide, not shortened or rewritten.
Getting passkeys to work on your phone: RP_ID, ORIGIN and HTTPS
This is where most first installs stall. Browsers only offer passkeys over HTTPS, with one exception: http://localhost. So the instance works on the computer running Docker, but your phone pointed at http://192.168.1.20:8080 will never show a passkey prompt. It is neither localhost nor HTTPS.
The fix is a real hostname with a certificate. The self-hosting guide covers three routes. Cloudflare Tunnel needs no open ports: create a tunnel and route gym.example.com to http://<docker-host>:8080. Caddy gets a Let's Encrypt certificate on its own with a two-line site block:
gym.example.com {
reverse_proxy localhost:8080
}
Traefik, nginx or Nginx Proxy Manager work too. If your proxy caps request bodies, allow at least 5 MiB on /api/, because the app syncs its whole history in one PUT. Then set RP_ID to the bare hostname and ORIGIN to the full URL, as in step 6 above. Pick the domain before anyone registers: changing RP_ID later invalidates every passkey bound to the old one.
Two escape hatches exist if HTTPS is not an option yet. Guest mode keeps data only in that browser, and PASSWORD_LOGIN=1 adds name-and-password sign-in next to passkeys. There is also a standalone Android app that needs no server at all, and it can pair with your instance through a one-time code from Settings.
Locking an openGym instance down for family or a small gym
Out of the box anyone who can reach the URL can create a profile, and each profile sees only its own data. For a shared instance, register your own profile first, copy your id from Settings, Account, Account ID, and add three lines to .env:
ADMIN_UIDS=youruserid # comma-separated; these users get the admin dashboard
INVITE_ONLY=1 # new profiles need an invite code
ALLOW_GUEST=0 # remove "Continue without account"
You get an admin dashboard for invite codes, disabling accounts and an activity log. Backups are a single archive of the data folder, which includes passkey public data and everyone's workout history:
RelatedPaperclip Setup: Self-Host an Org Chart for AI Agents
tar czf opengym-backup-$(date +%F).tar.gz data/
Guard the data/secret file in particular. It signs every session cookie, so losing it signs everyone out and un-pairs every phone.
openGym next to Strong, Hevy and wger
| Option | openGym | Strong / Hevy | wger |
|---|---|---|---|
| Where your data lives | JSON files in ./data on your server | The vendor's cloud account | Your server's database |
| License | AGPL-3.0 | Proprietary | Open source, self-hostable |
| Sign-in | Passkeys, optional password | Vendor account | Account on your instance |
| Imports history from | FitNotes, Strong, Hevy, Apple Health weight | Their own formats | Its own formats |
| Maturity | Repo created July 2026, releases about every two weeks | Years of polish, app-store apps | Long-running community project |
Our read: if you already run a home server or a Cloudflare Tunnel, openGym is worth installing today. The import path means trying it costs you nothing, and the guided workout flow aims at the polish of Strong, not the spreadsheet feel of older self-hosted trackers. If you have never touched a reverse proxy, the passkey requirement makes the first evening harder than the "one command" pitch suggests, so start with the Android APK or the demo. Be honest with yourself about storage, too. Everything lives in JSON files today, and the roadmap schedules a move to database storage in v1.4.0 in January 2027, flagged as the project's one compatibility break. The README is also open that the code is largely drafted with Claude Code under human review, with unit tests on the training logic. And the exercise images are third-party media with disputed ownership, downloaded by your instance rather than covered by the AGPL.
- v1.3.10, October 2026. Session queue and rotation, for routines that do not map to fixed weekdays.
- v1.4.0, January 2027. The switch to database storage. Take a
tarbackup of./databefore upgrading. - v1.4.1 to v1.4.3, early 2027. OIDC login and a trainer role, which would make shared gym instances far easier to run.
Fixing "verification failed", port 8080 clashes and missing exercise images
"verification failed" when signing in. RP_ID or ORIGIN does not match the address bar. Ask the server what it loaded:
docker compose logs api | grep 'gym-api on'
If that disagrees with your .env, the container is running the old environment. docker compose restart does not re-read .env; docker compose up -d does. Also check the shapes: RP_ID is a bare hostname with no scheme, port or trailing slash, while ORIGIN includes https:// and has no trailing slash.
No passkey prompt on the phone. You are on http:// or a LAN IP. Set up the HTTPS hostname from the section above, or switch on password sign-in.
Port 8080 is already in use. Set WEB_PORT=9090 in .env and change ORIGIN to match for local testing.
Exercise images never appeared. Check docker compose logs media, then re-run docker compose up -d or ./scripts/fetch-media.sh.
docker compose pull says "denied" or "unauthorized". Build from source instead with docker compose up -d --build.
- RepoDuarteSantos8/openGym on GitHub README, quick start, configuration reference, AGPL-3.0 license
- ReleaseopenGym releases v1.3.9, 28 September 2026, Android APK
- DocsSelf-hosting guide HTTPS options, multiple users, backups, troubleshooting
- DocsopenGym FAQ passkey failures, data location, updates
- DemoopenGym live demo the real app with example data
- DependencyDocker Docker Desktop or Engine with the Compose plugin
