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. $4 becomes 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-garminconnect library 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

References