Self-hosting Firefox Sync is a great way to keep bookmarks, passwords, tabs and history off Mozilla’s servers — but every fresh deployment of the popular docker-compose kit was failing logins with a 503 “Unexpected error: unable to get a node”. Containers healthy, database seeded, yet not a single account could ever connect. This post walks through the root cause, the fix, and proof from my production server.
This post was written with AI assistance (opencode) and reviewed before publishing.
The symptom
You deploy the stack, all containers come up healthy, the init container exits cleanly — and then Firefox refuses to sync:
Unexpected error: unable to get a node
No amount of wiping volumes and re-deploying helps. The database looks fine, the migrations run, the node table is populated. And yet: every login, 503.
The root cause
The tokenserver resolves which node should serve a user by looking up the sync-1.5 service in its database, then matching the nodes row against that service id.
The catch: on every fresh database, the upstream migrations guarantee that sync-1.5 lands at id 3 — the 2021 migration seeds ids 1–2, a later migration clears the table, and the 2026-02-26 migration re-inserts sync-1.5 without resetting the auto-increment counter.
The init script (db_init.sh), however, hardcoded 1:
-- BEFORE: hardcoded — never matches the real services id
INSERT IGNORE INTO services (id, service, pattern)
VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}');
INSERT INTO nodes (id, service, node, available, current_load, capacity, downed, backoff)
VALUES ('1', '1', '${DOMAIN}', '1', '0', '5', '0', '0')
ON DUPLICATE KEY UPDATE node='${DOMAIN}';
So nodes.service = 1 while services.id = 3. The lookup WHERE service = ? returns nothing, and the tokenserver correctly answers: unable to get a node. It’s a repo bug, not data drift — which is why resetting volumes reproduced it every time.
The fix
Resolve the service id dynamically instead of hardcoding it:
-- AFTER: resolved dynamically — works wherever sync-1.5 lands
INSERT INTO services (service, pattern)
SELECT 'sync-1.5', '{node}/1.5/{uid}' FROM DUAL
WHERE NOT EXISTS (SELECT 1 FROM services WHERE service = 'sync-1.5');
INSERT INTO nodes (id, service, node, available, current_load, capacity, downed, backoff)
VALUES ('1', (SELECT id FROM services WHERE service = 'sync-1.5'), '${DOMAIN}',
'1', '0', '5', '0', '0')
ON DUPLICATE KEY UPDATE node='${DOMAIN}',
service=(SELECT id FROM services WHERE service = 'sync-1.5');
The fix is upstreamed as porelli/firefox-sync#23 and shipped in the fork’s init image. While I was at it, the image workflow also learned the renamed SYNCSTORAGE_DATABASE_BACKEND / TOKENSERVER_DATABASE_BACKEND build-args for syncstorage-rs 0.23+ (porelli/firefox-sync#24) — without it, 0.23 images silently build for Google Spanner and panic on MySQL URLs.
Running in production
Here’s the stack on my server (real output), including the Exited (0) init container, the /__heartbeat__ health probe, and the tokenserver database showing exactly why the hardcoded id could never match:

Proof: a real Firefox client syncing
And the payoff — the client-side success log from my Firefox instance, syncing every few minutes against my server:

POST success 200 against firefox-sync.example.com, engines uploading, Sync completed … after 0.14 secs.
Deploy your own
The MySQL-backend images are published weekly as ghcr.io/tmiland/firefox-sync:
- Clone tmiland/firefox-sync and run
./prepare_environment.shto generate your.envand config templates docker compose up -d(put a reverse proxy in front — nginx example included)- Health check:
curl https://your-domain/__heartbeat__ - Point Firefox at your server:
about:config→identity.sync.tokenserver.uri→https://your-domain/1.0/sync/1.5(desktop; Android and iOS instructions in the README) - Optional: cap who can use your server with
MAX_USERS— implemented as a SQL trigger workaround, since syncstorage-rs has no native allowlisting
Credits & disclaimer
This is a fork of porelli/firefox-sync — all credit for the excellent deployment kit goes upstream; the fixes above are proposed back as #23 and #24. As upstream warns: the project is under development, not endorsed by Mozilla, and you shouldn’t rely on it as the sole store for your bookmarks and passwords.
Comments (via GitHub)