A free, open-source, self-hosted progressive overload tracker for strength training. Log every set, track weight and rep progression across all your exercises, and let the built-in algorithm decide when to increase load — no spreadsheets, no subscriptions, no cloud required.
Runs on a Raspberry Pi, a home server, or any machine with Node.js. Your data stays on your hardware.
![]() |
![]() |
![]() |
|---|
- Dynamic double progression — each set climbs toward its rep-range ceiling, then adds weight and resets — automatically, based purely on the reps you actually log. No subjective check-ins
- Per-exercise rep ranges — every exercise has an editable target rep range that drives its progression; seeded by movement type
- Workout plans — create named plans with configurable training days and exercises; run multiple plans (one active at a time)
- Calendar view — see every workout in a grid across weeks; navigate to any past or future session
- Per-session logging — log weight and reps per set; skip individual sets or entire sessions
- Actual-vs-target feedback — a per-set ▲ / = / ▼ glyph shows whether you beat, met, or missed each target
- Exercise history — per-exercise weight progression chart
- Plan cycling — when you finish the last session of a plan, the app prompts you to clone it into a new cycle with optional seed weights from any past week
- Multi-user — each user has their own account, plans, and data; no data is shared between users
- Equipment filter — select your available equipment; the exercise picker only shows relevant exercises
- Custom exercises — create exercises not in the built-in library
- Weight units — switch between kg and lbs; conversions are display-only, data is always stored in kg
- Dark theme — designed for gym lighting
FitnessTrack is a self-hosted alternative to apps like Hevy, Strong, and JEFIT for lifters who want full control over their data. No account required beyond your own server, no paywalled features, no ads.
- How the algorithm works
- Tech stack
- Installation
- Configuration
- Remote access
- HTTPS
- First run
- Updating
- Data
FitnessTrack uses dynamic double progression. Every exercise has a target rep range — e.g. 8–12 — and each set is its own progression track. There are no subjective check-ins: the next session's targets are derived purely from the reps you actually logged.
Comparing your actual logged reps against the target you were given:
| Situation | Next session |
|---|---|
| Hit the top of the rep range | Add weight by one increment; reps reset to the bottom of the range |
| Hit the target, still below the ceiling | +1 rep, same weight |
| Fell short of the target but stayed in range | Hold — same weight and target, try again |
| Couldn't reach the bottom of the range | Ease off — weight down one increment; reps reset to the bottom |
| Skipped | Unchanged |
Because each set advances independently, your first (freshest) set tends to climb fastest and earn a weight bump before the others — the "dynamic" in dynamic double progression. Later sets settle at their own level.
Weight increments are the exercise's configured step, capped at 10% of the working weight and rounded to the nearest 0.5 kg.
Each exercise carries its own rep range, seeded by movement type and editable any time — tap the ✎ on an exercise card or plan row:
| Movement type | Typical range |
|---|---|
| Heavy barbell compounds (deadlift, squat) | 4–8 |
| Moderate compounds (rows, presses) | 8–10 |
| Isolation (curls, extensions) | 8–12 |
| Small isolation (lateral raises, kickbacks) | 12–20 |
| Bodyweight | 12–20 |
| Session | You log | Next target |
|---|---|---|
| 1 | 20 × 9, 20 × 9, 20 × 8 | 20 × 10, 20 × 10, 20 × 9 |
| 2 | 20 × 12, 20 × 11, 20 × 10 | 21 × 8, 20 × 12, 20 × 11 |
In session 2 the first set reached the ceiling (12) → it adds weight and resets to the floor (21 × 8), while sets 2 and 3 keep climbing reps at 20 kg. The sets are now on slightly different weights — each progressing on its own.
While an exercise is in progress its card shows a progressive-overload hint — how this session's target compares to your last performance. Once logged, each set shows a ▲ / = / ▼ glyph for beat / met / missed versus its target.
Bodyweight exercises (pull-ups, dips, etc.) have no weight axis — reps simply climb toward the ceiling. Once every set reaches the ceiling, a set is added (up to six).
| Layer | Technology |
|---|---|
| Server | Node.js 20+, Express 4 |
| Database | SQLite (via better-sqlite3) |
| Auth | JWT in httpOnly cookies, bcrypt password hashing |
| Client | React 18, Vite 5, react-router-dom 6 |
| Charts | Recharts |
How serving works: The Express server on port 3001 serves both the API and the compiled React client — but only once the client has been built (npm run build). The built files live in client/dist/, which is not committed to the repository. The setup scripts for each platform run the build step automatically. In development (Option 4), the client is served by Vite on port 5173 instead, which proxies API calls to the Express server on 3001.
Docker is the easiest and most consistent deployment method. It works on Windows, Linux, and Raspberry Pi without any manual dependency management.
Prerequisites: Docker Desktop (Windows/Mac) or Docker Engine (Linux/Pi — see below).
git clone https://github.com/Gman0909/FitnessTrack.git
cd FitnessTrack
docker-compose up -dThe build runs automatically inside Docker (including the React client). Once complete, the app — frontend and API — is available at http://localhost:3001.
Important: Docker builds
better-sqlite3(a native addon) inside the container for the target architecture. Always build on the machine you intend to run on. Cross-compilation is not supported by the default setup.
To stop: docker-compose down
Data is stored in a Docker volume (fitness_data) and persists across container restarts and rebuilds.
To change the port: set PORT=8080 in a .env file in the project root before running docker-compose up.
Tested on Raspberry Pi OS (Bookworm/Bullseye), Raspberry Pi 3 and 4.
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
node --version # should print v20.x.x or highergit clone https://github.com/Gman0909/FitnessTrack.git
cd FitnessTrack
bash scripts/setup.shThis installs dependencies, builds the React client into client/dist/, and seeds the exercise library. The build step is required — without it the server has no frontend to serve.
npm startThe app — frontend and API — is available at http://localhost:3001, or from other devices on your network at http://<pi-ip-address>:3001.
The setup script handles this automatically — at the end of bash scripts/setup.sh it will ask if you want to install the service. It detects your username, working directory, and Node.js path automatically.
If you skipped that step or need to reinstall the service manually:
bash scripts/setup.sh # re-run and answer yes to the service promptOr install manually (replace the values to match your system):
sudo tee /etc/systemd/system/fitnesstrack.service > /dev/null <<EOF
[Unit]
Description=FitnessTrack
After=network.target
[Service]
Type=simple
User=$(whoami)
WorkingDirectory=$(pwd)
ExecStart=$(which node) server/index.js
Restart=on-failure
RestartSec=5
Environment=PORT=3001
Environment=DATABASE_PATH=$(pwd)/fitness.db
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable fitnesstrack
sudo systemctl start fitnesstrackCheck status: sudo systemctl status fitnesstrack
View logs: sudo journalctl -u fitnesstrack -f
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp dockerThen follow the Docker instructions above.
Prerequisites: Node.js 20 LTS (includes npm). During installation, check the box to install build tools (required for better-sqlite3).
Open Command Prompt or PowerShell:
git clone https://github.com/Gman0909/FitnessTrack.git
cd FitnessTrack
scripts\setup.batThis installs dependencies, builds the React client into client\dist\, and seeds the exercise library. The build step is required — without it the server has no frontend to serve.
scripts\start.batOr directly:
npm startThe app — frontend and API — is available at http://localhost:3001.
Install NSSM (Non-Sucking Service Manager), then:
nssm install FitnessTrack "C:\Program Files\nodejs\node.exe" "server\index.js"
nssm set FitnessTrack AppDirectory "C:\path\to\FitnessTrack"
nssm set FitnessTrack AppEnvironmentExtra PORT=3001
nssm start FitnessTrackUse this if you want to modify the code. Vite's dev server provides hot-reload for the client.
Note: In development, the frontend runs on port 5173 (Vite), not 3001. Port 3001 is the API only. Vite proxies all
/apirequests to the Express server automatically.
git clone https://github.com/Gman0909/FitnessTrack.git
cd FitnessTrack
npm run install:all # installs both server and client dependencies
npm run setup # create database and seed exercisesThen open two terminals:
# Terminal 1 — API server (port 3001)
npm run dev
# Terminal 2 — React dev server with hot-reload (port 5173)
cd client && npm run devOpen http://localhost:5173 in your browser.
Copy .env.example to .env and edit as needed:
PORT=3001
DATABASE_PATH=./fitness.db| Variable | Default | Description |
|---|---|---|
PORT |
3001 |
Port the server listens on |
DATABASE_PATH |
./fitness.db |
Path to the SQLite database file |
In Docker, DATABASE_PATH defaults to /data/fitness.db inside a persistent volume.
To access FitnessTrack from your phone or outside your home network without opening router ports, use Cloudflare Tunnel:
# Install cloudflared on Pi or your server machine
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main" | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt-get update && sudo apt-get install -y cloudflared
# Start a temporary public tunnel (no account needed for quick testing)
cloudflared tunnel --url http://localhost:3001This prints a temporary https://xxxx.trycloudflare.com URL you can open on any device. For a permanent URL, create a free Cloudflare account and set up a named tunnel.
FitnessTrack runs plain HTTP by default. If you need HTTPS — for example, to allow browsers to use the camera or vibration API, or to access the app from outside your network securely — you have two options:
Caddy handles TLS certificates automatically via Let's Encrypt.
# Install Caddy (Debian/Ubuntu/Pi)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddyCreate /etc/caddy/Caddyfile:
your-domain.com {
reverse_proxy localhost:3001
}
Then: sudo systemctl reload caddy
Caddy automatically obtains and renews the certificate. Replace your-domain.com with your actual domain (must be publicly reachable for Let's Encrypt).
sudo apt install -y nginx certbot python3-certbot-nginx
# Create site config
sudo tee /etc/nginx/sites-available/fitnesstrack <<'EOF'
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_cache_bypass $http_upgrade;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/fitnesstrack /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
# Obtain certificate (requires domain pointing to this machine)
sudo certbot --nginx -d your-domain.comCertbot modifies the nginx config to add HTTPS and sets up automatic renewal.
For LAN access only, use a self-signed certificate or mkcert. Note that browsers will show a warning for self-signed certs unless you import the CA into your devices.
- Navigate to the app in a browser
- Click Register and create your account (name, avatar glyph, username, password)
- Go to Setup → select the equipment you have access to
- Go to Plans → create a new plan, choose your training days
- Click into the plan → Configure → add exercises to each day
- Click Activate on the plan
- Go to Workout — your first session is ready to log
In-app (Linux / Raspberry Pi with systemd service):
If FitnessTrack is running as a systemd service, you can update it without a terminal. Navigate to Setup in the app → scroll to the bottom → click Check for updates. The app pulls the latest code, rebuilds the client, and restarts itself automatically. The page will reload once the new version is live.
This requires the server to be running under systemd with
Restart=on-failureso the process manager brings it back up after the restart.
Linux / Raspberry Pi (terminal) — run the update script (pulls, rebuilds, and restarts the systemd service if active):
bash scripts/update.shOr with the npm alias: npm run update
Windows:
scripts\update.batThen restart the server manually (npm start, or nssm restart FitnessTrack if running as a service).
Manual steps (any platform):
git pull
npm run install:all
npm run build
# Restart the serverWith Docker:
git pull
docker-compose up -d --buildThe exercise library (server/seed.js) is seeded automatically every time the server starts. New exercises added in updates will appear in your library after a restart — no manual migration needed. Existing exercises and all user data are preserved.
Custom exercises you create in the app are stored only in your local fitness.db and are never overwritten by updates.
All data is stored in a single SQLite file (fitness.db by default). To back it up, copy that file while the server is stopped. To restore, replace the file and restart.
# Backup
cp fitness.db fitness.db.backup
# With Docker (copy out of the volume)
docker run --rm -v fitnesstrack_fitness_data:/data -v $(pwd):/backup alpine \
cp /data/fitness.db /backup/fitness.db.backup

