Version: v0.0.14
A self-hosted fitness tracking system. Log workouts, build templates, sync Garmin watch data, and review everything in one dashboard — running entirely on your own server.
Magni integrates with two exercise data providers, both with free tiers:
AscendAPI — formerly ExerciseDB. Structured, expert-validated exercise data with GIFs, videos, instructions.
- Free tier: 2,000 requests/month
- RapidAPI: rapidapi.com/user/ascendapi
- GitHub: github.com/ExerciseDB/exercisedb-api
WorkoutX — 1,321 exercises with GIF animations, body part filters, target muscle data, equipment types, instructions.
- Free tier: 500 requests/month, no card required
- Docs: workoutxapp.com/docs.html
- Direct API (not RapidAPI)
You can use either, both, or neither — all keys are configured in the Admin UI.
| Service | Purpose |
|---|---|
magni_backend |
FastAPI API + React dashboard + backup scheduler |
magni_db |
PostgreSQL — all persistent data (local volume only) |
magni_redis |
Redis — sync queue and cache (local volume only) |
Important: Postgres and Redis data volumes must stay on local storage — they require POSIX file locking which CIFS does not support. Backup and media volumes support CIFS.
- Linux server with Docker Engine 24+ and Docker Compose v2
- A domain pointed at your server's public IP (or DDNS — DuckDNS is free)
- A reverse proxy handling TLS (Nginx Proxy Manager, Traefik, Cloudflare Tunnel, etc.) — optional for LAN-only use
- Ports 80 and 443 open on your router/firewall (if using public access)
git clone https://github.com/AshenKeep/magni.git
cd magni
cp .env.example .env
nano .env # fill in all values
docker compose pull
docker compose up -dOn first launch, the app detects no users exist and redirects to a setup page where you create your account. The setup page is inaccessible once an account exists.
After signing in, go to Admin → API Keys to add your provider keys (optional — only needed if you want to seed exercises from external sources).
cd magni
git pull
docker compose pull
docker compose up -dAPI keys for exercise providers (AscendAPI, WorkoutX) are NOT in .env — they're managed via the Admin UI and stored in the database. This avoids container restart hassles when changing keys.
| Variable | Description |
|---|---|
APP_URL |
Full public URL — must be https:// (or http:// for LAN) |
ALLOWED_ORIGINS |
CORS origins — usually same as APP_URL |
POSTGRES_DB |
PostgreSQL database name (default magni) |
POSTGRES_USER |
PostgreSQL username (default magni) |
POSTGRES_PASSWORD |
PostgreSQL password |
REDIS_PASSWORD |
Redis password |
SECRET_KEY |
JWT signing key — python3 -c "import secrets; print(secrets.token_hex(32))" |
ENVIRONMENT |
production or development |
BACKEND_PORT |
Port exposed to host (default 8000) |
TZ |
Timezone e.g. YOUR_TIMEZONE |
BACKUP_SCHEDULE |
Cron schedule (default 0 2 * * * — 2am daily) |
CIFS_PATH |
NAS backup share e.g. //YOUR_NAS_IP/backups |
CIFS_USERNAME |
NAS username |
CIFS_PASSWORD |
NAS password |
MEDIA_STORAGE |
external / local / cifs (default external) |
MEDIA_CIFS_PATH |
NAS media share (when MEDIA_STORAGE=cifs) |
MEDIA_CIFS_USERNAME |
NAS username for media |
MEDIA_CIFS_PASSWORD |
NAS password for media |
Magni can pull exercise data from AscendAPI and/or WorkoutX. Both have free tiers.
- Sign up for whichever provider(s) you want:
- AscendAPI: rapidapi.com → Search "EDB with Videos and Images by AscendAPI" → Subscribe (Basic, free)
- WorkoutX: workoutxapp.com/dashboard.html → Get API Key (free, no card)
- Go to Admin → API Keys in Magni
- Click "Add key" next to the provider, paste the key, save
- Go to Admin → Exercise Library — Seed, choose provider, click a seed button
| Mode | API requests | Result |
|---|---|---|
| Seed metadata only | ~9–10 | Exercise names, muscles, instructions. GIFs load from CDN |
| Seed + download GIFs | ~9 + N | Full data + GIFs cached on your server |
| Download GIFs for existing | ~N | Cache GIFs for already-seeded exercises |
Exercises are tagged with all muscle categories they target — primary, secondary, supporting. So filtering by "Chest" shows compound movements like push-ups (Chest + Shoulders + Core), not just isolation lifts.
Set MEDIA_STORAGE in .env:
external— GIFs load from provider CDN (default, no storage needed)local— GIFs downloaded to a local Docker volumecifs— GIFs downloaded to a CIFS NAS share
For cifs, also set MEDIA_CIFS_PATH/USERNAME/PASSWORD in .env and uncomment the CIFS block in docker-compose.yml.
docker compose exec backend python -m app.cli reset-password --email you@example.com --password newpassword
docker compose exec backend python -m app.cli create-user --email you@example.com --password newpassword --name "Your Name"
docker compose exec backend python -m app.cli list-usersMagni always runs HTTPS — there is no plain HTTP mode.
One-time setup required for every new deployment. You must run
gen-certs.shonce before starting the container. After that,docker compose pull && docker compose up -dis all you ever need for updates.
# Run from the directory containing docker-compose.yml
# Replace YOUR_SERVER_IP with your actual server IP, and YOUR_CERTS_PATH
# with wherever your compose volume mounts certs from on the host.
./scripts/gen-certs.sh YOUR_SERVER_IP /YOUR_CERTS_PATHThis creates three files in the specified directory:
cert.pem— server certificatekey.pem— private keyca.crt— install this on phones/tablets to trust the cert without warnings
Everyone running a new deployment needs to do this step once. The cert bakes in your server's IP address, so a pre-generated cert cannot be shared between deployments.
# .env settings
BACKEND_PORT=8443
SSL_CERTFILE=/certs/cert.pem
SSL_KEYFILE=/certs/key.pem
APP_URL=https://YOUR_SERVER_IP:8443
ALLOWED_ORIGINS=https://YOUR_SERVER_IP:8443
# Start (after gen-certs.sh has been run)
docker compose up -d
# Confirm
docker logs magni_backend | head -3
# → Starting HTTPS on port 8443Trust on devices (one time per device):
- Browser: visit the URL, click Advanced → Proceed
- iOS: serve
ca.crtover HTTP (python3 -m http.server 9000 --directory /YOUR_CERTS_PATH), open that URL on the device, install the profile, then Settings → General → About → Certificate Trust Settings → enable trust - Android: transfer
ca.crtto the device → Settings → Security → Install certificates → CA certificate
Caddy handles TLS automatically including Let's Encrypt cert renewal.
# .env: SSL vars still required (uvicorn terminates TLS internally)
BACKEND_PORT=8443
SSL_CERTFILE=/certs/cert.pem
SSL_KEYFILE=/certs/key.pem
# Caddyfile (minimal):
# your.domain.com {
# reverse_proxy localhost:8443
# }server {
listen 443 ssl;
server_name gym.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/gym.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/gym.yourdomain.com/privkey.pem;
location / {
proxy_pass http://localhost:8443;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Pangolin handles ingress — point it at localhost:8443. Leave SSL_CERTFILE/SSL_KEYFILE blank so uvicorn runs plain HTTP internally (Pangolin terminates TLS at the edge).
Backups run automatically per BACKUP_SCHEDULE. As of v0.0.9 they are written as magni_backup_YYYYMMDD_HHMMSS.tar.gz, each tarball containing:
db.sql— pg_dump outputmanifest.json— backup metadata + media file list with sizes/mtimesmedia/...— copy of/media(only if "Include media" is enabled in Admin → Backup)
Retention is configurable from Admin → Backup → Settings (default 7 most recent backups). Older backups are pruned after each run.
Restore from the UI: Admin → Backup → pick a backup → Restore. This drops the public schema, replays db.sql, and (if media is in the tarball) replaces /media. Restore is destructive and cannot be undone — confirm carefully.
Manual restore from CLI (rare — UI is preferred):
# Extract the archive somewhere temporary
tar -xzf magni_backup_YYYYMMDD_HHMMSS.tar.gz -C /tmp/restore
# Replay the SQL
docker compose exec -T db psql -U magni -c "DROP SCHEMA IF EXISTS public CASCADE; CREATE SCHEMA public;"
docker compose exec -T db psql -U magni magni < /tmp/restore/db.sql
# Restore media (if present in the archive)
rm -rf /path/to/media && cp -r /tmp/restore/media /path/to/mediadocker compose logs -f backend # live logs
docker compose restart backend # restart backend
docker compose exec db psql -U magni magni # database shell
docker compose down # stop everything
docker compose down -v # stop and wipe all data (irreversible)Available at http://localhost:8000/api/docs when ENVIRONMENT=development.
| Method | Path | Description |
|---|---|---|
GET |
/api/auth/setup-required |
First-run setup check |
POST |
/api/auth/setup |
Create first account |
POST |
/api/auth/login |
Get JWT token |
GET |
/api/auth/me |
Current user |
GET |
/api/dashboard/ |
Summary stats |
POST |
/api/workouts/ |
Create workout |
GET |
/api/workouts/ |
List workouts |
GET |
/api/workouts/{id} |
Workout detail |
PATCH |
/api/workouts/{id} |
Update workout |
DELETE |
/api/workouts/{id} |
Delete workout |
POST |
/api/workouts/{id}/sets |
Add set |
PATCH |
/api/workouts/{id}/sets/{set_id} |
Update set |
DELETE |
/api/workouts/{id}/sets/{set_id} |
Delete set |
POST |
/api/workouts/{id}/save-as-template |
Convert a logged workout into a reusable template |
POST |
/api/exercises/ |
Add exercise |
GET |
/api/exercises/ |
List exercises |
PATCH |
/api/exercises/{id} |
Update exercise |
DELETE |
/api/exercises/{id} |
Delete exercise |
POST |
/api/templates/ |
Create template (typically with no exercises — add them after) |
GET |
/api/templates/ |
List templates |
GET |
/api/templates/{id} |
Get template with exercises and per-set targets |
PATCH |
/api/templates/{id} |
Update template name/notes |
DELETE |
/api/templates/{id} |
Delete template |
POST |
/api/templates/{id}/exercises |
Add an exercise (with per-set targets) to a template |
PATCH |
/api/templates/{id}/exercises/{te_id} |
Edit log type or sets on a template-exercise |
DELETE |
/api/templates/{id}/exercises/{te_id} |
Remove an exercise from a template |
POST |
/api/templates/{id}/start |
Start workout from template (pre-fills sets you can edit) |
POST |
/api/stats/daily |
Upsert Garmin daily stats |
GET |
/api/stats/daily |
Query daily stats |
POST |
/api/stats/hr |
Bulk insert HR readings |
POST |
/api/sync/ |
Batch sync from Android |
GET |
/api/admin/api-keys |
List configured provider keys |
POST |
/api/admin/api-keys |
Save/update provider key |
DELETE |
/api/admin/api-keys/{provider} |
Remove provider key |
GET |
/api/admin/backup/status |
Backup status |
GET |
/api/admin/backup/list |
List available backups |
GET |
/api/admin/backup/settings |
Get retention/include_media settings |
PATCH |
/api/admin/backup/settings |
Update retention/include_media |
POST |
/api/admin/backup/run |
Manual backup (optional include_media body) |
POST |
/api/admin/backup/restore/{filename} |
Restore from a backup (DESTRUCTIVE) |
DELETE |
/api/admin/backup/{filename} |
Delete a backup file |
POST |
/api/exercises/{id}/upload-image |
Upload PNG/JPG/GIF/WEBP image (≤5 MB) |
GET |
/api/admin/exercises/seed/estimate |
Quota estimate |
POST |
/api/admin/exercises/seed |
Seed exercises (provider param) |
POST |
/api/admin/exercises/download-gifs |
Cache GIFs locally |
GET |
/api/admin/exercises/media/status |
Media storage status |
GET |
/api/admin/logs/seed |
Last 10 seed attempts |
GET |
/health |
Health check + version |
See CHANGELOG.md.