Six months ago, the thought struck me: why not track my lifting too? I didn't want to be locked into Strong or Hevy, because that's where my data lives in the cloud. So I went looking for a self-hosted solution — and stumbled across LiftTrace, a new weightlifting tracker under AGPL that runs in a single Docker container.
In this article, I'll show you how I set up LiftTrace, what trade-offs I made, and how I wired it into my existing pipeline. Plus: what you can take away from the setup for your own monitoring.
Where we started
I've been doing strength training for about ten years — some periods more consistently than others. I kept logging data throughout, though. At first in an Excel spreadsheet, then two years in Strong, then a year in Hevy. On average that's been three to four workouts a week, so roughly 200 sessions a year, trending upward.
In late summer 2024, Hevy changed its pricing model: free users suddenly only saw the last 50 workouts in their cloud history. Anyone who wanted the full history had to pay $30 a year. That's not a lot of money — but it annoyed me. I had over 1,200 workouts in the database that I'd curated over the years, and I was supposed to pay to keep seeing them?
But the real problem wasn't the money. The real problem was: I didn't have a complete export. Hevy did offer a CSV export feature, but it was clunky and incomplete. Strong had already taken similar steps back then (before the Hevy deal). The trust issue is real: your training history is part of your identity as an athlete. If a vendor goes bankrupt, triples its prices, or just pulls the plug, you're the one left holding the bag.
So: DIY it is. I wanted my data at home. SQLite, one container, done. No cloud, no subscription trap, no marketing emails.
What I didn't want: a heavy enterprise solution. No Kubernetes clusters, no Postgres replication, no OAuth2 servers with third-party login. I'm a one-person project with two to four workouts a week — the setup should match that.
Requirements
Before I installed LiftTrace, I wrote a short list of requirements. Here it is, lightly anonymized:
- Single-container deployment. I want a single
docker runcommand and that's it. No Compose file with seven services, no external databases. - Local persistence. SQLite is enough. My workouts are tiny — at 1,500 entries, the database is under a megabyte.
- AGPL or similarly open. MIT or BSD would have been my preference, but I'll accept AGPL as long as I don't have to offer the product publicly.
- API or webhook. I want to be able to read the data without opening a browser. Otherwise I can't feed it into my Watch & Alert pipeline.
- Optional AI. A feature that analyzes my training and spits out recommendations would be nice — but only if I can switch it off. Running an AI server at home is a luxury, not a must.
- Bodyweight and reps. Sounds trivial, but surprisingly many trackers don't have a decent data model for auxiliary lifts, progression schemes, or plate math.
What I didn't put on the list: a mobile app. I take notes on my phone while training, but it doesn't have to be a native app. A responsive web UI is enough for me.
Three options compared
After my initial search, I landed on three realistic options. I'm naming them openly here, with their pros and cons.
Option 1: Keep going with the spreadsheet. I could have just gone back to my old Excel spreadsheet. Works offline, is exportable, no cloud. But: a spreadsheet template doesn't scale when you want to track progressive overload, need plate math, and want to log multiple sets with different weights per exercise. I'd have had to build my own template, which is nothing but a bad database. I discarded this option after two hours.
Option 2: LiftTrace, the new self-hosted tracker. LiftTrace was introduced on r/selfhosted in late 2025. Single container, SQLite inside, optional LLM integration. License: AGPLv3. What appealed to me: the author makes it clear that he's positioning the tracker as a response to Strong and Hevy — exactly the problem I had. The data model is reasonably documented, and there's a REST API. Downsides: the project is young, so no big community yet, and AGPL restricts commercial use. For a private hobby, that doesn't matter.
Option 3: Build it myself with FastAPI and SQLite. I could have written a minimal tracker in a weekend: FastAPI backend, HTMX frontend, SQLite file. That's tempting because I'd get exactly the data model I want. But honestly: I didn't want to spend my time on form validation, but on training. And if LiftTrace already covers 80 percent of my requirements, building my own only makes sense if I want to learn something from it — not if I just need a tool.
I went with Option 2, with a small side path toward Option 3: if LiftTrace doesn't do what I need in some place, I'll write a wrapper or modify the source code. AGPL explicitly allows that for private use.
The solution: LiftTrace in a container, wired up to the pipeline
Installing LiftTrace is unremarkable, which is a good thing. One docker run call, a volume mount for the SQLite file, a port mapping. In my setup, the container runs on the same host as my other self-hosted stuff — a small Hetzner server with 8 GB of RAM that also runs Watch & Alert, Paperless, and a few other containers.
docker run -d \
--name lifttrace \
-v /opt/lifttrace/data:/data \
-p 8080:8080 \
-e LIFTTRACE_API_KEY=<geheimer-key> \
ghcr.io/lifttrace/lifttrace:latest
I put the API key in my secrets.env, which only root can read. Then I set up my exercises and programs once in the browser — bench press, squat, deadlift, plus a 5/3/1 scheme and a push-pull-legs plan. That initial configuration took roughly 30 minutes.
The interesting part was wiring it up to my Watch & Alert pipeline. LiftTrace has a REST API, but no webhook yet. So I wrote a small poller that runs every 30 minutes via cron and checks whether there's a new workout in the system.
from __future__ import annotations
import json
import logging
import os
from datetime import datetime, timedelta
from pathlib import Path
import httpx
logging.basicConfig(
level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s"
)
log = logging.getLogger("lifttrace-watcher")
LIFTTRACE_URL = os.environ["LIFTTRACE_URL"]
LIFTTRACE_API_KEY = os.environ["LIFTTRACE_API_KEY"]
WATCH_ALERT_FEED = Path("/opt/watchalert/feeds/lifttrace.json")
def fetch_recent_workouts() -> list[dict]:
headers = {"Authorization": f"Bearer {LIFTTRACE_API_KEY}"}
cutoff = (datetime.utcnow() - timedelta(days=2)).isoformat()
params = {"since