| backups | ||
| config | ||
| fastapi | ||
| backup_db.sh | ||
| Daniela_com.samsung.health.weight.20251110133813.csv | ||
| docker-compose.yml | ||
| gewicht.csv | ||
| import_samsung_scale_csv.sh | ||
| import_scale_csv.sh | ||
| init.sh | ||
| Lars_com.samsung.health.weight.20251204225581.csv | ||
| preview_samsung_scale_import.sh | ||
| preview_scale_csv_import.sh | ||
| README.md | ||
| restore_db.sh | ||
Garmin Log
A single Docker Compose project that downloads Garmin Connect data for multiple users, stores it in PostgreSQL, and provides provisioned Grafana dashboards for individual and comparative analysis.
Components
api: FastAPI application and scheduled Garmin Connect importerpostgres: normalized statistics plus complete raw JSON responsesgrafana: provisioned PostgreSQL datasource and dashboards
Writable container state is kept in data/api, data/postgres, and
data/grafana. Read-only application configuration is kept in config.
The importer stores activity summaries, activity details, splits, weather, exercise sets, daily summaries, sleep, heart rate, stress, body battery, SpO2, respiration, HRV, training readiness, weight, user profile, and device data. Unsupported endpoints or data absent from a user's Garmin account are skipped without failing the whole sync.
Garmin Connect does not provide a supported public API for complete personal
data export. This project uses the unofficial garminconnect Python client.
Endpoint behavior can change, and Garmin may require reauthentication or apply
rate limits.
Setup
Run the initialization script:
./init.sh
It creates .env and config/users.yml when absent, creates the writable
directories with the required container ownership, creates .venv, and
installs all FastAPI runtime and test dependencies.
Then:
-
Set strong PostgreSQL and Grafana passwords in
.env. -
Add each Garmin account and its credentials to
config/users.yml:users: - id: alice display_name: Alice email: alice@example.com password: replace-with-real-password enabled: trueThe stable
idis used in database rows, token paths, and Grafana filters; changing it creates a new logical user. -
Start the project:
docker compose up --build -d -
Open:
- Grafana: http://localhost:3000
- FastAPI documentation: http://localhost:8000/docs
The container IDs used by init.sh are API 65534:65534, PostgreSQL Alpine
70:70, and Grafana 472:0. config/users.yml is owner-readable and
group-readable by the API container (640); .env is owner-only (600).
The first import runs on startup. By default it imports all available activity
pages and 365 days of daily health data. Historical activity detail is fetched
once and retained. Later syncs stop after reaching a page of known activities
and refresh the most recent 14 days of daily data. Garmin
authentication tokens are retained in data/api so
scheduled imports do not need a fresh password login every time.
Garmin tokens are stored per user under data/api/tokens/<user-id> and survive
API container recreation and host restarts because data/api is a bind mount.
Credentials remain in config/users.yml; they are consulted only when no valid
token can be loaded or Garmin invalidates an existing token.
Configuration
Key .env values:
| Variable | Default | Purpose |
|---|---|---|
GARMIN_INITIAL_DAYS |
365 |
Daily history fetched until the first successful sync |
GARMIN_SYNC_DAYS |
14 |
Daily lookback after the first successful import |
GARMIN_SYNC_SCHEDULE |
0 */6 * * * |
Cron schedule |
SYNC_ON_STARTUP |
true |
Run an import when API starts |
TZ |
Europe/Berlin |
Scheduler and container timezone |
API_PORT |
8000 |
Host API port |
GRAFANA_PORT |
3000 |
Host Grafana port |
The initial import can be large. To reduce API load, set
GARMIN_INITIAL_DAYS=30. Activity history remains exhaustive up to the safety
limit of 10,000 activities (100 pages of 100).
Multi-user comparison
The Garmin Overview dashboard has a multi-select Users variable. Choose
one, several, or all users to compare activity distance and resting heart rate.
Each row in the Activities table has a Details link to the same activity
detail dashboard used for shared-run comparisons.
The Overview also includes raw intraday heart rate, raw HRV, daily average HRV, and a one-row-per-night sleep table. Sleep rows include timing, score, phase percentages, SpO₂, respiration, stress, restlessness, awakenings, and sleep need where Garmin provides them. Sleep phases are shown as a 100%-stacked horizontal bar inside each sleep-table row. Click Details to open the nightly sleep dashboard with a sleep-stage timeline, phase-duration chart, heart rate, HRV, and the complete normalized night summary.
The Shared Runs dashboard pairs runs from selected users when:
- start times differ by no more than 15 minutes, and
- recorded distances differ by no more than 0.5 km.
Both thresholds are adjustable dashboard variables. This is deliberately an inference because Garmin activities do not consistently expose participant relationships.
Paces are always shown as min:sec /km, including graph axes, tooltips,
legends, and tables. Click Details on a shared-run or Overview activity row
to open an individual detail view or comparison with pace, heart rate, GPS
tracks, elevation, cadence, ground contact time, vertical ratio, and summary
statistics. The detail view
automatically uses the time range
from the earliest sample to the latest sample across both runners. Activity
sample data is extracted from Garmin's
stored activity-detail payloads; run a sync after upgrading to backfill samples
for details that were downloaded previously.
The Garmin Sync Status dashboard shows running imports, the latest
successful import, recent failures, and up to 500 sync-history rows. Use the
Trigger sync for all users link to schedule every configured user. Set the
API URL dashboard field if the API is not available at
http://localhost:8000. When a sync reports mfa_required, select MFA user,
enter the numeric MFA code, and use the dashboard's Submit MFA code link.
The accepted code refreshes the persisted token and retries that user's sync.
The dashboard refreshes every 10 seconds.
API
GET /healthGET /api/usersPOST /api/syncPOST /api/sync/{user_id}?days=30GET /api/auth/{user_id}/mfaPOST /api/auth/mfaPOST /api/auth/{user_id}/mfaGET /api/activities?user_id=alice&user_id=bobGET /api/sync-runs
The API is not authenticated. Do not expose port 8000 publicly without putting it behind an authenticated reverse proxy.
Garmin MFA
When Garmin requires MFA, the sync run ends with status mfa_required. Check
the status and submit the numeric code received from Garmin:
curl http://localhost:8000/api/auth/alice/mfa
curl -X POST http://localhost:8000/api/auth/alice/mfa \
-H 'Content-Type: application/json' \
-d '{"code":"123456"}'
In FastAPI Swagger at http://localhost:8000/docs, use
POST /api/auth/mfa and enter both values:
{
"user_id": "alice",
"code": "123456"
}
Submitting a valid code stores refreshed Garmin tokens in data/api/tokens and
automatically schedules the user's sync again. Scheduled imports then reuse
those tokens without asking for MFA until Garmin invalidates them.
The pending MFA challenge is held in API memory because it contains an active Garmin login session. If the API container restarts while waiting for the code, trigger another user sync to create a new challenge:
curl -X POST http://localhost:8000/api/sync/alice
Operations
View importer status:
curl http://localhost:8000/api/sync-runs
docker compose logs -f api
Trigger a user import:
curl -X POST 'http://localhost:8000/api/sync/alice?days=30'
Stop containers without deleting data:
docker compose down
Delete all PostgreSQL and Grafana data:
docker compose down -v