Skip to content

Garmin Grafana

Pulls fitness data from Garmin Connect for one or more users and stores it in a local InfluxDB instance for visualization in Grafana. Runs in the home namespace.

Architecture

graph LR
    subgraph garmin["Garmin Connect (cloud)"]
        GC[Garmin Connect API]
    end

    subgraph home["Namespace: home"]
        FHD[garmin-fetch-hd\nFenix 7S]
        FXKH[garmin-fetch-xkh]
        IDB[InfluxDB 1.11\nport 8086]
    end

    subgraph nas["Synology NAS"]
        BCK[(NFS backup\npvc-garmin-influxdb-backup)]
    end

    subgraph grafana["Grafana"]
        DASH[Dashboard]
    end

    GC -->|pull via garmin-fetch-data| FHD
    GC -->|pull via garmin-fetch-data| FXKH
    FHD -->|write GarminStats_hd| IDB
    FXKH -->|write GarminStats_xkh| IDB
    IDB -->|local-path PVC| worker-1a[(worker-1a)]
    worker-1a -->|hourly rsync| BCK
    IDB -->|InfluxDB datasource| DASH
Hold "Alt" / "Option" to enable pan & zoom

Components

garmin-fetch-data

One deployment per user. Each pod runs garmin-fetch-data, which authenticates to Garmin Connect, downloads activity and health data (steps, sleep, heart rate, workout FIT files), and writes it to InfluxDB.

Setting hd xkh
Deployment garmin-fetch-hd garmin-fetch-xkh
Device Fenix 7S
InfluxDB database GarminStats_hd GarminStats_xkh
Secret garmin-hd-secret garmin-xkh-secret
Token path garmin-tokens PVC · hd/ subPath garmin-tokens PVC · xkh/ subPath

ALWAYS_PROCESS_FIT_FILES=True is set so workout files are re-processed on restart, preventing data gaps after pod restarts.

Password encoding

The ExternalSecret template base64-encodes the Garmin password before injecting it, because the container requires GARMINCONNECT_BASE64_PASSWORD rather than a plaintext credential.

InfluxDB

InfluxDB 1.11 stores all time-series fitness data. HTTP auth is enabled; the garmin user has per-database write access.

Setting Value
Image influxdb:1.11
Port 8086
Index tsi1
Node worker-1a (pinned via nodeSelector)
Storage garmin-influxdb-data PVC — 10Gi local-path

local-path is used instead of NFS because InfluxDB's WAL and file locking are incompatible with NFS — corruption can occur. The node is pinned to worker-1a so the local PVC and the backup CronJob always land on the same node.

Adding a new user database

The INFLUXDB_DB env var only creates the initial database (GarminStats_hd). Additional databases must be created manually:

kubectl exec -n home deploy/garmin-influxdb -- \
  influx -username admin -password <pw> \
  -execute 'CREATE DATABASE GarminStats_xkh'
kubectl exec -n home deploy/garmin-influxdb -- \
  influx -username admin -password <pw> \
  -execute 'GRANT ALL ON GarminStats_xkh TO garmin'

Storage

PVC Class Size Purpose
garmin-influxdb-data local-path 10Gi Live InfluxDB data on worker-1a
garmin-influxdb-backup syno-nfs-retain 10Gi Hourly rsync target on Synology NAS
garmin-tokens syno-nfs-retain (RWX) 10Mi Shared OAuth token store; one subPath per user

The token PVC is ReadWriteMany so multiple fetch pods can mount it simultaneously, each reading their own subdirectory.

Backup

A CronJob runs hourly on worker-1a, rsyncing the live InfluxDB data to the NFS backup PVC:

schedule: "0 * * * *"
rsync -av --delete /data/ /backup/

Both the CronJob and the InfluxDB deployment are pinned to worker-1a via nodeSelector — this is required because local-path PVCs are node-local and both jobs must access the same volume.

Secrets

Secret Vault path Keys
garmin-influxdb-secret garmin/influxdb admin-password, user-password
garmin-hd-secret garmin/hd email, base64-password (auto-encoded by ESO template)

See Security for ExternalSecrets details.

Adding a new user

  1. Add Garmin credentials to Vault at garmin/<username>
  2. Add an ExternalSecret for garmin-<username>-secret (see commented template in externalsecret.yaml)
  3. Copy deployment-hd.yamldeployment-<username>.yaml, updating the deployment name, label, subPath, INFLUXDB_DATABASE, and secret reference
  4. Create the InfluxDB database manually (see tip above)
  5. Add the new deployment to kustomization.yaml