My local-only Syncthing was still phoning home
A while back I set up Syncthing to run entirely on my LAN: local discovery on, global discovery and relaying off, nothing announcing my devices to the internet. I even made a point of it in that post, that you should trust the listen addresses, not the checkboxes. It turns out I hadn’t looked hard enough at my own setup.
What gave it away, months later, was my Pi-hole query log. I was digging through it for something unrelated when I saw a box that’s supposed to be local-only quietly resolving discovery-announce-v4.syncthing.net and discovery-announce-v6.syncthing.net, again and again. Those are Syncthing’s global discovery servers. The ones I’d turned off.
The checkbox was telling the truth, just not the whole truth
In the web UI, global discovery was unchecked. The config agreed:
<options>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<globalAnnounceServer>default</globalAnnounceServer>
<relaysEnabled>false</relaysEnabled>
</options>
globalAnnounceEnabled was false. But look at the line under it. The <globalAnnounceServer>default</globalAnnounceServer> entry was still there. default is a placeholder Syncthing expands to its public discovery servers, and in my case Syncthing kept resolving those hostnames even with announcing disabled. Turning the feature off in the GUI flips the enabled flag; it doesn’t remove the server list. The list is what was generating the DNS lookups.
So the listen-addresses check I’d been so pleased with wasn’t the full story either. The device wasn’t announcing itself, but it was still doing name resolution against the discovery servers in the background. The only place that showed up was DNS.
The fix: strip the leftover server list
The config lives at /opt/syncthing/config/config.xml in my Docker setup. Syncthing owns that file and rewrites it, so the safe order is stop, edit, start, not edit-while-running.
Back it up first:
cp /opt/syncthing/config/config.xml /opt/syncthing/config/config.xml.bak
Then strip every globalAnnounceServer element. With xmlstarlet it’s one clean delete:
xmlstarlet ed -L -d "/configuration/options/globalAnnounceServer" /opt/syncthing/config/config.xml
If you don’t have xmlstarlet, sed does the job by deleting the element’s lines:
sed -i '/<globalAnnounceServer>/,/<\/globalAnnounceServer>/d' /opt/syncthing/config/config.xml
Then start the container again:
docker compose -f /opt/syncthing/docker-compose.yml up -d
I folded this into the same script that disables global discovery and relaying, so a fresh deploy strips the server list instead of leaving it behind for me to find in the logs later.
Verifying it actually stopped
This is the part the GUI can’t tell you, so go back to DNS. After the restart I watched the Pi-hole query log for the Syncthing host and the lookups to discovery-announce-v4.syncthing.net and discovery-announce-v6.syncthing.net were gone. No more announce traffic, no more resolution. That, not a checkbox, is how I know it’s local-only now.
Lessons
- Disabling a feature in a GUI isn’t the same as removing its config. Syncthing’s global-discovery toggle sets
globalAnnounceEnabled=falsebut leaves<globalAnnounceServer>in place, and that list still drove DNS lookups. - DNS logs are the honest audit for “is this really local-only.” A service can look disabled in its own UI and still be resolving public hostnames. Your resolver sees what the app actually does.
- Let the service own its config file. Syncthing rewrites config.xml, so stop the container before editing, then start it again, or your change can get overwritten.
- Bake the fix into deployment. Once I knew the flag alone wasn’t enough, removing the server list became part of the setup script, not a thing I rediscover in the query log.
Related reading
Self-hosting file sync with Syncthing, kept local-only
I wanted Dropbox-style file sync across my own devices without putting the files on anyone else's servers, and without announcing my devices to the internet. Here is how I deployed Syncthing on Docker-in-LXC and ran it entirely on my LAN.
Recovering Syncthing from a truncated config.xml
Syncthing didn't crash, it went unhealthy: the container kept running but couldn't parse its own config. The file had been cut in half by an interrupted write. Here's how I read the symptoms and restored it from Syncthing's own backup.
Self-hosting Backlogia, and fixing it before running it
Backlogia is a self-hosted app that pulls your game libraries from Steam, GOG, Epic and more into one place. Before I would run it I read the code, found four security gaps, and forked it. Then Starlette and a CORS bug had opinions too.
Ready to Transform Your Career?
Let's work together to unlock your potential and achieve your professional goals.