Syncthing Peer-to-Peer Sync - Continuous Replication Without a Server
Status: Active
Last Updated: 2026-08-26
Category: Cloud - File Synchronization
Prerequisites: linux-fundamentals, docker-compose-intro
Time: 2-3 hours
Tags: syncthing, sync, peer-to-peer, replication, systemd
Summary
Syncthing provides continuous, encrypted, peer-to-peer folder synchronization between devices β no central server required. This article covers deploying Syncthing on the homelab server (native systemd and Docker variants), sharing folders between peers, ignore patterns, understanding relay vs direct connections, and writing a hardened systemd unit.
π― What You'll Learn
By the end of this article, you'll be able to:
- β Install and run Syncthing as a systemd service
- β Share folders between two or more devices securely
- β
Write
.stignorepatterns that keep caches out of sync traffic - β Distinguish relayed from direct connections and force direct on LAN
- β Choose between file sync and true backup (and why you need both)
Table of Contents
- Where Syncthing Fits
- Installation & systemd Unit
- Docker Deployment
- Folder Sharing & Device Pairing
- Ignore Patterns
- Relay vs Direct Connections
- Versioning
Context / Why This Matters
Syncthing solves a different problem than Nextcloud: there is no web UI, no accounts, no shares-with-links β just device-to-device replication where every peer holds a full copy of every shared folder. That makes it ideal for keeping a laptop's working set mirrored to the homelab NAS continuously, which in turn feeds the restic backup pipeline (restic-backups). It also removes Nextcloud from the critical path for bulk data that never needs to be shared via URL.
Important framing: Syncthing is sync, not backup. Delete a file on your laptop, and it is deleted everywhere within seconds β including the server copy. That is exactly why it must feed into versioned backups per storage-backup-strategies, and why its built-in file versioning should be enabled.
Implementation / Core Content
Installation & systemd Unit
On Debian/Ubuntu hosts:
curl -fsSL https://syncthing.net/security-key.txt | gpg --dearmor \
| sudo tee /usr/share/keyrings/syncthing-archive-keyring.gpg >/dev/null
echo "deb [signed-by=/usr/share/keyrings/syncthing-archive-keyring.gpg] https://apt.syncthing.net/ syncthing stable" \
| sudo tee /etc/apt/sources.list.d/syncthing.list
sudo apt update && sudo apt install syncthing
Upstream ships a unit for user-level operation (syncthing@.service):
sudo systemctl enable --now syncthing@agentic.service
systemctl status syncthing@agentic
The web GUI listens on 127.0.0.1:8384. For remote administration over SSH only:
ssh -L 8384:127.0.0.1:8384 server.fogserv.cloud
# then open http://localhost:8384 locally
If you expose the GUI over the network instead (reverse proxy with TLS per ../security/tls-configuration.md), immediately set an admin username/password and keep GUI β Listen address off 0.0.0.0 unless proxied.
A hardened drop-in override if you prefer a system-level unit:
# /etc/systemd/system/syncthing.service
[Unit]
Description=Syncthing continuous file sync
After=network-online.target
Wants=network-online.target
[Service]
User=syncuser
Group=syncuser
ExecStart=/usr/bin/syncthing serve --gui-address=127.0.0.1:8384 --no-browser
Restart=on-failure
SuccessExitStatus=3 4
RestartForceExitStatus=3 4
# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only
ReadWritePaths=/home/syncuser/.local/state/syncthing /srv/syncthing
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload && sudo systemctl enable --now syncthing
Docker Deployment
For consistency with other fogserv.cloud services (docker-compose-intro):
services:
syncthing:
image: syncthing/syncthing:latest
hostname: fogserver
environment:
- PUID=1000
- PGID=1000
volumes:
- ./config:/var/syncthing/config
- /srv/syncthing:/var/syncthing/data
ports:
- "22000:22000/tcp" # sync protocol (TCP)
- "22000:22000/udp" # sync protocol (QUIC)
- "21027:21027/udp" # local discovery
- "127.0.0.1:8384:8384" # GUI, loopback only; proxy or SSH-tunnel it
restart: unless-stopped
Run one Syncthing instance per host β running both the native package and the container causes index lock conflicts.
Folder Sharing & Device Pairing
Pairing flow (do this once per device pair):
- On the server: Actions β Show ID, copy the device ID (a long cryptographic identifier).
- On the laptop: Add Remote Device, paste ID, name it
fogserver. - The server shows a "New Device" prompt β accept, and check Auto Accept only if you trust the peer's folder layout.
- On either side: Add Folder (e.g.,
/srv/syncthing/laptop-docs), set sharing with the remote device. - The other side accepts the folder share and chooses its local path.
Key settings per folder worth changing from defaults:
- File Versioning β Staggered File Versioning: keeps old versions (max age configurable). This converts accidental deletes/overwrites from data loss into a minor inconvenience.
- Watch for Changes (default on): near-real-time propagation via inotify.
- Full Rescan Interval: raise to 3600s+ for huge trees since watching handles incremental changes.
- Send/Receive vs Send Only: make the server copy Receive Only for laptopβserver flows so a misbehaving laptop cannot propagate deletions upstream without explicit approval ("Revert Local Changes").
CLI equivalents exist for headless automation:
syncthing cli show system
syncthing cli config devices add --device-id "<ID>"
Ignore Patterns
.stignore lives at the root of each synced folder and prevents matching files from propagating at all β they are neither sent nor tracked.
// .stignore example for a developer laptop folder
*.tmp
~$*
.DS_Store
Thumbs.db
node_modules
.git/cache
target/
dist/
*.iso
.stfolder // marker dir; ignoring it breaks the folder β don't
Syntax essentials:
- Patterns are relative paths;
foomatches any depth,/fooonly the root (?d)prefix = deletable: safe to remove these files when cleaning up ignored junk!pattern= exception (re-include); exceptions must come after broader ignores
Gotcha: ignores are per-device configuration, not synced content. If laptop and server disagree on .stignore, you get index divergence and endless rescans. Keep a canonical .stignore checked into git and distribute it.
Relay vs Direct Connections
Syncthing tries, in order: direct TCP/QUIC to discovered addresses, then hole-punched NAT traversal, finally relays (volunteer servers that bounce encrypted traffic).
Check connection quality in the GUI (device row) or:
syncthing cli show connections | jq '.connections[] | {addr: .address, type: .type}'
TCP-LAN/QUIC-LAN: direct local link β best throughput, use for NAS syncsTCP (relay): works through anything but slow and latency-heavy; fine for small text folders, painful for media
To prioritize LAN and discourage relays:
- Enable QUIC listener (UDP 22000) β better NAT traversal, fewer relay fallbacks
- On the server's device config for each peer: set static addresses
tcp://laptop.lan:22000, quic://laptop.lan:22000, dynamic - Global discovery can stay on as fallback; relays are used only as last resort
Relayed traffic remains end-to-end encrypted (relays see ciphertext only), so privacy is preserved β the cost is purely performance.
Versioning
Even with restic downstream, enable per-folder versioning as the first line of defense against sync-propagated mistakes:
| Type | Behavior |
|---|---|
| Trash Can | Deleted/old files moved to .stversions/ |
| Simple | Keep N versions of changed files |
| Staggered | Hourly/daily/weekly aging β recommended default |
| External | Call a script per change |
Staggered versions live under <folder>/.stversions/ and are excluded from sync by design.
Practical Examples
Example 1: Laptop β NAS mirror feeding restic
# Server side: receive-only folder at /srv/syncthing/laptop-home,
# staggered versioning, 30 days max age.
# Then restic picks it up nightly:
restic backup /srv/syncthing --tag syncthing-mirror
Result: laptop loss or ransomware costs at most seconds of sync lag plus whatever versions/restic retention preserves β three independent recovery paths.
Example 2: Verify a healthy direct LAN connection
syncthing cli show connections | jq '.total'
# expect: {"at": "...", "inBytesTotal": ..., "outBytesTotal": ...}
# In GUI: Devices β fogserver-laptop β "Address" should show tcp://192.168.x.x:22000
If you see relay://... on a same-site peer, fix discovery/firewall before syncing large trees.
Example 3: Recover an overwritten document from versions
ls -la /srv/syncthing/laptop-docs/.stversions/report.odt*
cp .stversions/report.odt~2026-08-20-14-33-02 ~/restore-report.odt
Copy it back into the folder and Syncthing redistributes the restored version to all peers.
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
| Peers connect via relay despite LAN | UDP 22000/21027 blocked; discovery broken | Open firewall ports; add static device addresses |
| Endless re-scanning / index churn | Mismatched .stignore between peers |
Distribute identical .stignore to all devices |
| Files owned by wrong UID in container | PUID/PGID unset or mismatched | Set PUID/PGID env to owning user; chown -R once |
| Sync storm on start | Full rescan of huge tree | Raise full-rescan interval; ensure watch enabled |
| Deletions propagated everywhere instantly | No versioning; folder not receive-only | Enable staggered versioning; make server side Receive Only |
| Two instances fighting on one host | Native service + container both running | Pick one; disable the other permanently |
| GUI unreachable remotely | Bound to loopback by default | SSH tunnel or authenticated TLS reverse proxy |
Next Steps / Ops Actions
- Feed all synced server folders into the restic pipeline: backup-to-object-storage
- Schedule and alert on the resulting jobs: backup-automation
- If exposing the GUI externally, follow tls-configuration
- Review the sync-vs-backup distinction in storage-backup-strategies
Sources & Related Articles
External references consulted:
- https://docs.syncthing.net/users/setup.html
- https://docs.syncthing.net/users/ignoring.html
- https://docs.syncthing.net/users/faq.html
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.