Syncing Withings to Garmin Connect with Docker on Unraid
Automated sync from a Withings scale to Garmin Connect using withings-sync running as a self-contained Docker container on Unraid. The container handles its own scheduling via supercronic — no host-level cron required.
Architecture Overview
+-------------------+ OAuth2 token +----------------------+ FIT file upload +-------------------+
| | | | | |
| Withings Scale | --------------------> | withings-sync | -----------------------> | Garmin Connect |
| (Body+ / Body | Withings API | (Docker, Unraid) | python-garminconnect | |
| Cardio) | | supercronic 3hr | | |
| | | --config-folder | | |
+-------------------+ +----------------------+ +-------------------+
|
/mnt/user/appdata/
withings-sync/config/
├── .withings_user.json
├── .garmin_session.json
└── entrypoint.sh
Background
Withings and Garmin don’t talk to each other natively. The official workaround is routing through a third-party service, which means your health data leaving your network to bounce through someone else’s infrastructure. withings-sync solves this by authenticating directly with both APIs and pushing weight data as a FIT file to Garmin Connect — the same format Garmin’s own devices use.
The container uses supercronic for internal scheduling, which means it runs as a persistent container on its own cadence rather than a run-and-exit job that needs an external scheduler. Everything persists in a mapped config directory so tokens survive container restarts and image updates.
Stack
| Component | Role |
|---|---|
| withings-sync | Sync engine — Withings API to Garmin Connect |
| supercronic | In-container cron scheduler |
| python-garminconnect | Garmin auth and FIT file upload |
| Unraid Docker GUI | Container management |
| /appdata/withings-sync/config | Token and config persistence |
Implementation
01 — Create the Config Directory
Create the config directory and entrypoint script before starting the container. Docker will create directories as root if they don’t already exist, causing permission issues.
1mkdir -p /mnt/user/appdata/withings-sync/config
Create entrypoint.sh with a randomized cron schedule to avoid Garmin SSO rate limiting (hitting Garmin more than ~8 times per day triggers 429/401 errors):
1cat > /mnt/user/appdata/withings-sync/config/entrypoint.sh << 'EOF'
2#!/bin/sh
3echo "$(( $RANDOM % 59 +0 )) */3 * * * poetry run withings-sync --config-folder /config" > /home/withings-sync/cronjob
4supercronic -debug -passthrough-logs /home/withings-sync/cronjob
5EOF
The $RANDOM % 59 picks a random minute offset so every deployment doesn’t hammer Garmin at :00 past the hour.
02 — Add the Container in Unraid GUI
In Docker → Add Container, set:
| Field | Value |
|---|---|
| Repository | ghcr.io/jaroslawhartman/withings-sync:latest |
| Extra Parameters | --entrypoint sh |
| Post Arguments | /config/entrypoint.sh |
Add environment variables:
| Variable | Value |
|---|---|
TZ |
America/New_York |
GARMIN_USERNAME |
your Garmin Connect email |
GARMIN_PASSWORD |
your Garmin Connect password |
Add volume mappings:
| Host Path | Container Path |
|---|---|
/mnt/user/appdata/withings-sync/config |
/config |
/etc/localtime |
/etc/localtime (read-only) |
Do not start the container yet.
03 — One-Time Withings Authorization
The first run requires interactive OAuth2 — withings-sync will print a URL, you authorize in the browser, and paste the token back within 30 seconds.
1docker run -it --rm \
2 -v /mnt/user/appdata/withings-sync/config:/config \
3 -e GARMIN_USERNAME="your@email.com" \
4 -e GARMIN_PASSWORD='yourpassword' \
5 ghcr.io/jaroslawhartman/withings-sync:latest \
6 --config-folder /config
Single-quote the password. If your password contains
$,&, or!, double quotes will cause bash to interpret those characters before passing them to Docker.$4becomes an empty variable,&forks the process. Always use single quotes for passwords with special characters in shell commands.
Open the Withings URL in a browser, authorize the app, copy the token from the redirect page, and paste it at the Token : prompt.
04 — Garmin Authentication
After Withings auth succeeds, the same run attempts Garmin login. A few notes:
- MFA: If your Garmin account has email verification enabled, the
python-garminconnectlibrary can’t complete the flow non-interactively. Temporarily disable it in your Garmin account security settings, complete the auth run, then re-enable it. The saved session token persists and MFA re-enabled on the account won’t affect it. - Rate limiting: Repeated failed login attempts trigger Garmin’s IP-based rate limiter, which returns 429 and then falls through to a misleading 401. If you see this, stop retrying and wait 2+ hours. Each retry resets the cooldown.
- 429 in successful runs: The library tries multiple login methods in sequence (mobile, widget, portal). 429 warnings on the first two methods are normal — the portal method succeeds and the warnings can be ignored.
A successful run looks like:
INFO - Garmin authentication successful
INFO - Fit file with weight information uploaded to Garmin Connect
INFO - Saving Last Sync
05 — Verify Token Files and Start the Container
1ls -la /mnt/user/appdata/withings-sync/config/
2# .withings_user.json
3# .garmin_session.json
4# entrypoint.sh
With both token files present, start the container from the Unraid GUI. Confirm supercronic is running and has scheduled the first job:
1docker logs withings-sync --tail 20
Expected output:
level=info msg="read crontab: /home/withings-sync/cronjob"
level=debug msg="job will run next at 2026-06-10 18:12:00 +0000 UTC" job.command="poetry run withings-sync --config-folder /config"
Gotchas
Breaking change in v6: withings-sync v6 migrated Garmin authentication from the deprecated garth library to python-garminconnect. Old .garmin_session files from garth are not compatible — one fresh login is required after upgrading. After that, tokens refresh automatically.
Driver version on entrypoint: The --entrypoint sh and /config/entrypoint.sh in Post Arguments must be split across Extra Parameters and Post Arguments respectively in the Unraid GUI. Putting the full --entrypoint "sh /config/entrypoint.sh" in Extra Parameters as a single string causes a “Bad parameter” error — Unraid’s Docker template parser doesn’t handle it correctly.
Token files owned by a named user: The interactive auth run creates token files owned by the user inside the container. This is fine — the scheduled runs in the persistent container use the same UID and can read them without issue.
Outcomes
| Sync frequency | Every 3 hours, randomized minute |
| Auth | One-time interactive setup, tokens auto-refresh |
| Privacy | Direct API-to-API, no third-party relay |
| Cost | Free |
| Persistence | Tokens survive container restarts and image updates |