workout-tracker: a self-hosted Go app for GPX runs, route segments and daily stats
A workout tracking web application for personal use (or family, friends), geared towards running and other GPX-based activities
At a glance
- What is it?
- jovandeginste/workout-tracker is a single-binary web application that ingests GPX, TCX and FIT files, stores them alongside manual workouts and daily body metrics, and renders route progress on Leaflet maps. It is built for one person or a small household, not for a training platform with an audience.
- Who is it for?
- Adopt it if you already own your activity files and want a private place to put them, with segment matching and a heatmap over the same data. Do not adopt it if you need multi-tenant accounts with per-user isolation, or if you expect a hosted mobile app.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The gap workout-tracker fills: your activity files, on your own disk
Most running and cycling data ends up inside a service you do not control. The README describes workout-tracker as a workout tracking web application for personal use, or for family and friends, geared towards running and other GPX-based activities, self-hosted with everything included. That last phrase is the design brief. There is no cloud component and no account on someone else's server. You point a browser at http://localhost:8080 and the data lives in a directory you mounted.
The audience is narrow on purpose. Someone who records runs with a watch or phone, exports GPX, TCX or FIT files, and wants to keep a decade of them in one queryable place. The README also lists manual workout records for weight lifting, push-ups and swimming, so the app is not strictly a GPS archive. Daily stats such as weight and step count can be entered by hand or pushed through the API. If you want social features, leaderboards or a coach's view of your training load, the feature list does not contain them.
How the pieces fit: Go server, SQLite or Postgres, Leaflet on top
The repository is Go, and the deployment story matches that: a single binary that creates a database file in the current directory and starts a web server. The Docker images are published for amd64 and arm64, and the compose files come in two variants, docker-compose.sqlite.yaml and docker-compose.postgres.yaml, layered on a shared docker-compose.base.yaml. The database choice is therefore a deployment decision made before the first start, not a runtime setting buried in a config file.
The frontend stack is visible in the repository topics: leaflet for maps, tailwindcss for styling. The README's screenshot section describes a single workout view with a zoomable, draggable map of the GPX track and more details per point, plus summarized statistics. There is also a Swagger specification at docs/swagger.json, validated by a badge in the README, which means the HTTP API is documented as a contract rather than reverse engineered from the UI.
The mechanism worth understanding is segment matching. You create route segments, and according to the README the application will try to detect matches of your workouts against them. That is a comparison between an uploaded track and stored segment geometry, run at import time. The README does not state the matching tolerance, the algorithm, or whether partial segment coverage counts. If segment detection matters to you, that is the first thing to read in the source.
Getting it running: docker run, compose, or a downloaded binary
The shortest path is the container. The README gives this example, mounting the current directory as the data directory and publishing port 8080:
docker run -p 8080:8080 -v .:/data ghcr.io/jovandeginste/workout-tracker:release
Tags follow a pattern: latest for master builds, release for the newest release, and version tags such as 2, 2.0 and 2.0.2. The README notes that latest and release images are available for amd64 and arm64. Running as a non-root user is documented too, with -u 1000:1000 and the caveat that the mounted directory must be owned by uid 1000.
Sessions need a key. Without one, the README implies sessions do not persist across restarts. Two environment variables cover it: WT_JWT_ENCRYPTION_KEY for an inline secret, or WT_JWT_ENCRYPTION_KEY_FILE pointing at a file, typically mounted from /run/secrets. The compose route downloads three files, docker-compose.base.yaml plus either the sqlite or the postgres variant, and for Postgres also postgres.env, whose parameters you edit before docker compose up -d.
The native route is a tarball from the releases page. The README's example for v2.0.2 on Linux x86_64 downloads workout-tracker-v2.0.2-linux-amd64.tar.gz, extracts it and runs ./workout-tracker. The same WT_JWT_ENCRYPTION_KEY or WT_JWT_ENCRYPTION_KEY_FILE variables apply. Registration defaults are worth noting: new users have to be activated by an admin, and registration can be disabled entirely.
Where it stops being the right tool
Multi-user support exists, but the README frames the product as personal use, or family and friends. The login page description says new users must be activated by an admin, which is a household-scale control, not a tenant model. Nothing in the supplied material describes per-user data isolation guarantees, quotas, or an admin audit trail. If you are considering this as a small club platform, you are extrapolating past the documentation.
The licence metadata is contradictory and you should resolve it yourself. The repository metadata reports NOASSERTION, while the README carries an MIT badge linking to opensource.org. Those two signals disagree, and I cannot tell you from the supplied material which one governs the code you would ship. Read the LICENSE file in the repository before you redistribute anything.
Two operational gaps are visible. First, the README's Docker examples mount a directory but do not describe a backup or migration procedure for the SQLite file, so the durability of your archive is your problem. Second, the project explicitly asks users to donate workout files because it is collecting real files for testing purposes, including raw files from devices. That tells you the parser surface is broad and continuously exercised against new inputs, which is good for correctness over time but also means file-format edge cases are an active area rather than a settled one. If your device produces something unusual, expect to be the one who finds the bug.
How it differs from a general fitness dashboard
The obvious comparison is a self-hosted personal dashboard such as Grafana fed by a database, or a general quantified-self tracker. The difference is in what the app knows about a workout. A dashboard gives you numbers you have to model yourself: distance, duration, heart rate as time series. workout-tracker stores the track geometry and reasons about it. Route segments are first-class objects, and the README says the application tries to detect matches of your workouts against them. It also keeps track of equipment you are using, so a shoe or a bike is an entity you attach to activities, not a tag you type.
The heatmap feature is the other divergence. It answers where have you been, a lot, which is a spatial aggregation over every stored track. A time-series dashboard cannot produce that without you writing the geospatial query layer. Conversely, a dashboard can chart anything you can express as a query, while workout-tracker is bounded by its own feature list: uploads, manual records, daily stats, segments, equipment, statistics, heatmap. If your question is outside that list, the API and the database are the escape hatch, and the Swagger spec at docs/swagger.json is where you start.
Upgrade and maintenance cost
The release cadence visible in the supplied data is roughly monthly across v2.7.0, v2.8.0 and v2.9.0, with the last push to master in September 2026. That is an actively developed project, which cuts both ways: you get fixes, and you get schema churn. The README does not describe a migration path between versions, so the practical upgrade procedure is to stop the container, back up the data directory, pull the new tag, and start it again. Whether the application applies database migrations on boot is not stated in the material I have.
Pin your tag. Running ghcr.io/jovandeginste/workout-tracker:latest means you are tracking master builds, and the README distinguishes latest from release precisely because they are different things. For an archive you intend to keep, the release or a specific version tag is the safer default, and the version tags 2, 2.0 and 2.0.2 show the project maintains moving minor and major pointers as well as exact versions.
The JWT encryption key is the one piece of state that is not in the data directory. Rotate it or lose it and existing sessions are invalidated. The README's file-based variant, WT_JWT_ENCRYPTION_KEY_FILE, exists so the secret can live in a Docker secret or a mounted file rather than in a shell history, which is the pattern to prefer if you run this on anything reachable from a network.
Who this is for, and what to check before you commit
This fits a runner or cyclist who already exports files from a watch or phone, wants them on their own hardware, and is comfortable with docker run or a compose file. It also fits a household where a few people share one instance and an admin activates accounts, which is exactly the model the login page describes. The presence of an API and a documented Swagger spec makes it reasonable for someone who wants to push data from a phone app automatically, as the README's Fitotrack example suggests is possible.
It does not fit anyone who needs a mobile app as the primary interface, since the README describes a web application and a browser at port 8080. It does not fit anyone who needs contractual clarity on the licence before deployment, given the MIT badge and the NOASSERTION metadata disagree. And it does not fit anyone who wants a hosted service with an uptime commitment, because there is none.
Verify three things first. Run the container without WT_JWT_ENCRYPTION_KEY, log in, restart it, and see whether you are still logged in; that tells you what the key actually protects. Decide between docker-compose.sqlite.yaml and docker-compose.postgres.yaml before you import anything, because switching later is a data migration you would be writing yourself. Then upload one GPX file from your own device and check that the map renders and the statistics match what your watch reported. If that file imports cleanly, the rest of the archive will too.
Editorial conclusion
Adopt it if you already own your activity files and want a private place to put them, with segment matching and a heatmap over the same data. Do not adopt it if you need multi-tenant accounts with per-user isolation, or if you expect a hosted mobile app. Before committing, verify how the WT_JWT_ENCRYPTION_KEY behaves when you restart the container without it, confirm which database backend your deployment will use, and read the LICENSE file in the repository, because the GitHub metadata reports NOASSERTION while the README badge says MIT.
Community notes