Syncthing keeps folders in sync across your machines without a cloud account, a subscription, or anyone else's servers touching your files. It's peer-to-peer: your laptop talks to your desktop talks to your phone, encrypted end to end. The one weakness of a pure peer-to-peer setup is obvious the moment you think about it - two devices can only sync when they're both online at the same time. Close the laptop before the desktop wakes up and the changes just wait.
A VPS fixes that. Park a Syncthing node on a server that never sleeps and it becomes the always-on hub every other device syncs against. Your phone uploads a photo, the VPS has it seconds later, and your desktop pulls it down whenever it next comes online - no overlap required. The same node doubles as an off-site backup of every folder you point at it.
This guide sets up Syncthing on a single VPS with Docker, puts the admin GUI behind Caddy with automatic HTTPS, and walks through pairing devices, one-way folders, file versioning, and the encrypted "untrusted node" trick that lets you store data on a server without trusting the server with it.
docker-compose.yml22000 TCP and UDP); keep the GUI internalsync.example.com with auto-HTTPS and a passwordTotal time: about 25 minutes.
80 and 443 open for Caddy, plus 22000 TCP/UDP for sync trafficSyncthing is light on CPU and RAM but it scans files, so give it disk headroom: the data you sync plus a little room for the version history you'll enable later.
In your DNS provider, add an A record for the GUI:
sync.example.com → YOUR_VPS_IPV4
Add an AAAA record too if you have IPv6. Confirm it resolves before going further:
dig +short sync.example.com
The sync traffic itself does not need DNS - devices find each other by ID through Syncthing's global discovery. The subdomain is only for reaching the admin interface.
On a fresh Ubuntu box:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify:
docker --version
docker compose version
Syncthing needs two things open from the outside: Caddy's web ports for the GUI, and the sync transport port.
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 22000/tcp
sudo ufw allow 22000/udp
sudo ufw enable
Port 22000/tcp carries the main sync stream and 22000/udp carries QUIC, which Syncthing prefers when it can. You do not need to open 21027/udp - that's local LAN discovery and is useless across the internet. You also do not need 8384 open; the GUI stays behind Caddy.
sudo mkdir -p /opt/syncthing
cd /opt/syncthing
sudo mkdir -p config data caddy-data caddy-config
config/ - Syncthing's identity, keys, and folder config (the part you can't lose)data/ - the actual synced filescaddy-* - Caddy's certificate and stateCreate /opt/syncthing/docker-compose.yml:
services:
syncthing:
image: syncthing/syncthing:latest
container_name: syncthing
hostname: vps-node
restart: unless-stopped
environment:
PUID: "1000"
PGID: "1000"
volumes:
- ./config:/var/syncthing/config
- ./data:/var/syncthing/data
ports:
- "22000:22000/tcp"
- "22000:22000/udp"
networks:
- sync-net
caddy:
image: caddy:2
container_name: syncthing-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- sync-net
networks:
sync-net:
The GUI port 8384 is intentionally not published to the host. Caddy reaches it over the internal sync-net network, so the only way in from the internet is through HTTPS with a password.
hostname: vps-node is what shows up as this device's name in the GUI - pick something you'll recognize.
Create /opt/syncthing/Caddyfile:
sync.example.com {
reverse_proxy syncthing:8384
}
Caddy requests a Let's Encrypt certificate on first boot and renews it forever. Syncthing's GUI is a long-lived single-page app with an event stream, and Caddy proxies that without any extra tuning. If you want the full reverse-proxy pattern, see our Caddy guide.
cd /opt/syncthing
sudo docker compose up -d
sudo docker compose logs -f
Watch the logs until Caddy reports a certificate and Syncthing prints My ID: .... That long string is this node's device ID - the public key fingerprint other devices use to find and trust it. Copy it; you'll need it in a moment. You can always read it back later:
sudo docker exec syncthing syncthing --device-id
Open https://sync.example.com. The web interface loads, but right now it has no password - anyone who finds the URL can reconfigure your sync. Fix that immediately.
In the GUI, go to Actions -> Settings -> GUI and set:
Save. The page reloads and asks for the credentials.
If the GUI shows a host check error through the proxy, add this under the GUI settings or set it once via the config: tell Syncthing to skip the host header check, since Caddy is the real front door. The cleanest way is the environment variable - add STGUIADDRESS: "0.0.0.0:8384" to the compose environment block and recreate the container.
Syncthing pairing is mutual: each side adds the other's device ID, and both must accept. Say you're connecting your laptop.
On your laptop, open its Syncthing GUI and copy its device ID from Actions -> Show ID.
On the VPS GUI, click Add Remote Device, paste the laptop's ID, give it a name like laptop, and save.
Back on the laptop, a prompt appears asking whether to add the new device that just contacted it (the VPS). Accept it, name it vps-node, and save.
Within a few seconds both GUIs should show the other device as Connected. If they sit on "Disconnected" for more than a minute, jump to Troubleshooting - it's almost always the sync port.
Pairing connects devices; sharing connects folders. On the device that has the data - say the laptop holds ~/Documents - do this:
Documents) and note the Folder ID - it must match on every device that shares it.vps-node. Save.On the VPS GUI a notification appears: the laptop wants to share Documents. Accept it. Set the folder path on the VPS to something under the synced data volume, for example /var/syncthing/data/Documents, and save.
The first sync scans and transfers everything. After that, only changed blocks move, so subsequent syncs are near-instant.
By default folders are bidirectional - a change anywhere propagates everywhere, including deletes. That's perfect for working files but risky for a backup node: delete a file on your laptop by accident and the VPS dutifully deletes its copy too.
To make the VPS hold everything and never push changes back, set its copy of the folder to Receive Only:
Now the VPS accepts incoming changes but never sends its own, and if something does diverge it surfaces a "Revert Local Changes" button instead of silently overwriting. Pair this with versioning in the next step and the VPS becomes a genuine safety net, not just a mirror.
The mirror image is Send Only, useful when the VPS is the source of truth - say it generates reports other machines should pull but never modify.
Versioning is what turns sync into something you can recover from. Without it, an overwrite or delete that syncs is gone everywhere. With it, the VPS keeps old copies.
On the VPS copy of the folder, edit it and open File Versioning. Pick one:
For a backup target, Staggered with a max age of 90 days is a sensible default. Versions live in a .stversions folder inside the synced directory, so they count against disk - budget for it.
This is the trick that makes a VPS a great Syncthing target even if you don't fully trust the host: untrusted (encrypted) devices. When you mark a remote device as untrusted for a folder, your other devices encrypt the data with a password before sending it. The VPS stores only ciphertext - it can sync the blocks but cannot read a single file.
To enable it, on a trusted device (your laptop), edit the folder, open the Sharing tab, and next to vps-node set an encryption password. Every trusted device sharing that folder must use the exact same password. The VPS itself is never given the password; it just relays and stores encrypted blocks.
Untrusted mode is ideal for a cheap or shared VPS, or any server you'd rather not have plaintext on. The cost is that the VPS GUI can no longer show file names or restore versions in the clear - recovery happens on a trusted device.
The synced files protect themselves by being on multiple devices. The one thing that's unique to this node is /opt/syncthing/config - it holds the device identity and keys. Lose it and the VPS gets a new device ID, which means re-pairing everything.
Create /usr/local/bin/syncthing-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
BACKUP_DIR="/var/backups/syncthing"
DATE="$(date +%F)"
mkdir -p "$BACKUP_DIR"
tar -czf "$BACKUP_DIR/syncthing-config-$DATE.tar.gz" -C /opt/syncthing config
find "$BACKUP_DIR" -name "syncthing-config-*.tar.gz" -mtime +14 -delete
Make it executable and schedule it:
sudo chmod +x /usr/local/bin/syncthing-backup.sh
echo "15 4 * * * root /usr/local/bin/syncthing-backup.sh" | \
sudo tee /etc/cron.d/syncthing-backup
For off-site copies of the actual data, point restic at the data directory - Syncthing handles live sync, restic handles cold, immutable snapshots.
Syncthing upgrades are uneventful:
cd /opt/syncthing
sudo docker compose pull
sudo docker compose up -d
Take a config backup first. Because the config is a plain bind mount, a bad upgrade is a docker compose down, restore the tarball, pin the previous image tag, and up -d.
Devices stay "Disconnected." The sync port isn't reachable. Confirm 22000/tcp and 22000/udp are open in UFW and in your provider's external firewall, then check the Listeners in the GUI under Actions -> Status show as online rather than LISTEN_ERROR.
GUI shows a "Host check error." Syncthing is rejecting the proxied Host header. Set STGUIADDRESS: "0.0.0.0:8384" in the compose environment block and recreate the container, or disable the GUI host check in settings.
Caddy can't get a certificate. DNS hasn't propagated, or ports 80/443 are blocked upstream. Verify with dig and check your provider's firewall panel.
A folder is stuck at "Out of Sync." Usually a permissions mismatch - the container's PUID/PGID don't own the data directory. Run sudo chown -R 1000:1000 /opt/syncthing/data and rescan.
Sync is slow over the internet. If devices fall back to a relay, throughput drops. Make sure at least one side has the sync port directly reachable; the VPS is the natural candidate since it has a public IP and an open 22000.
.stignore file in a folder keeps node_modules, build artifacts, and caches out of sync to save bandwidth and disk.That's the whole setup. One always-on VPS turns Syncthing from "sync when two devices happen to overlap" into a real hub-and-spoke system, with versioned history and optional zero-knowledge encryption, and not a cloud subscription in sight.
Need an always-on box to anchor your sync setup? Our Linux plans include fast NVMe storage, IPv6, and generous bandwidth - ideal for a Syncthing hub that's online around the clock. See the options.