Private peer-to-peer file replication - self-hosted, no central server.
No file data passes through a central server. The registry only brokers peer discovery; all transfers are direct peer-to-peer via libtorrent.
Two sync modes:
| Mode | Registry needed | Discovery | Access control |
|---|---|---|---|
registry |
Yes | Registry + LAN | ACL enforced by registry |
local |
No | LAN multicast only | Anyone on LAN with the share_id |
curl -fsSL https://raw.githubusercontent.com/theronconrey/peerdup/main/install.sh | shThe installer asks if you also want to install the registry on this machine. Say yes on your always-on server; say no on laptops and workstations that only sync files. Works on Fedora, Ubuntu/Debian, and macOS.
LAN-only? If all your peers are on the same network, skip the registry entirely and use
--localshares instead.
Docker instead? If you prefer to run the registry and relay in containers with automatic TLS (Let's Encrypt), see the Docker deployment section below.
On the machine where you installed the registry:
peerdup-registry-setupPrompts for port and database path, then starts the registry in the background. Subsequent runs restart it without re-prompting.
peerdup-setupPrompts for your machine name, registry address, relay/LAN settings, and optionally CA/client certificate paths for mTLS. Starts the daemon. Leave registry blank if you're using local-only shares.
To change settings later (add a relay, update the registry address, etc.):
peerdup-setup --updateRuns the same wizard with all current values pre-filled. Press Enter to keep any setting, or type a new value to change it.
# Machine A - create share and grant access
peerdup share create photos ~/Pictures
peerdup share grant photos <machine-b-peer-id>
# Machine B - join
peerdup share add <share_id> ~/Pictures
peerdup share peers photos# Machine A
peerdup share create photos ~/Pictures --local
# -> prints share_id
# Machine B (same LAN)
peerdup share add <share_id> ~/Pictures --localpeerdup/
├── registry/ # Registry server - peer discovery, ACL, presence
├── relay/ # Relay server - TCP rendezvous for symmetric NAT
├── daemon/ # Peer daemon + CLI
├── start.sh # Server-side first-run setup + docker compose launcher
├── docker-compose.yml # Registry + relay + Caddy
├── Caddyfile # Caddy TLS config
└── .env.example # Copy to .env if you prefer manual config
Share commands accept either the share name or the full share_id.
peerdup identity
peerdup share list
peerdup share info <name-or-id>
peerdup share peers <name-or-id>
peerdup share create <name> <path> [--local] [--import-key <file>] [--conflict <strategy>]
peerdup share add <share_id> <path> [--local] [--conflict <strategy>]
peerdup share grant <name-or-id> <peer_id>
peerdup share revoke <name-or-id> <peer_id>
peerdup share remove <name-or-id>
peerdup share set-limit <name-or-id> --up 10M --down 50M
peerdup share set-conflict <name-or-id> last_write_wins|rename_conflict|ask
peerdup share conflicts <name-or-id>
peerdup share resolve <conflict-id> keep-local|keep-remote
peerdup share pause <name-or-id>
peerdup share resume <name-or-id>
peerdup status
peerdup watch
peerdup registry health # probe registry status (version, uptime, peer counts)
peerdup registry status # show daemon's registry connection and TLS configThe socket path is auto-detected from $XDG_RUNTIME_DIR (user installs) or
/run/peerdup/control.sock (system installs). Override with PEERDUP_SOCKET.
peerdup share list shows a MODE column (registry or local) and a PEERS
column formatted as active/announced. peerdup status shows global transfer
rates when any share is actively syncing.
Changes on any peer replicate to all others - there is no designated source-of-truth machine. Every peer can read and write.
Each local change increments a monotonic sequence number. When two peers have different versions of a share, the one with the higher sequence number wins. This means the most recently modified peer's state propagates rather than an arbitrary one.
Folder renames and file moves are efficient: when a peer receives a new torrent layout, peerdup matches existing files by name and size and moves them into place before libtorrent checks pieces. Files that don't need to change don't transfer at all - they just get repositioned. Stale files and empty directories from prior layouts are cleaned up automatically.
When two peers independently modify the same share before either has seen the other's changes, peerdup detects the divergence via mismatched sequence numbers and info-hashes and applies the share's conflict strategy:
| Strategy | Behaviour |
|---|---|
last_write_wins |
Accept the remote version silently (default) |
rename_conflict |
Rename local files to name.conflict.TIMESTAMP.PEERID.ext, then accept the remote |
ask |
Pause the share and record the conflict; resolve manually |
peerdup share create docs ~/Documents --conflict rename_conflict
peerdup share set-conflict docs ask
peerdup share conflicts docs
peerdup share resolve 3 keep-local
peerdup share resolve 3 keep-remotepeerdup watch emits [CONFLICT] events in real time.
Share owners can publish advisory rate limits from the registry. All members
receive the policy immediately via the live peer event stream and apply it to
their libtorrent handle. Local per-share caps (peerdup share set-limit) always
take precedence - if both are set, the more restrictive value wins. Local-only
shares ignore registry policy entirely.
Policy is advisory: daemons are expected to honor it but are not required to.
For peers behind symmetric NAT that can't connect directly, the relay is
included in the Docker stack and exposed on TCP port 55002. Enable it via
peerdup-setup prompts or manually in config.toml:
[relay]
enabled = true
address = "your-domain:55002"The daemon tries a direct connection and a relay bridge simultaneously. libtorrent uses whichever connects first.
On GNOME desktops (Fedora, Ubuntu GNOME, etc.), peerdup includes a top-bar extension that shows sync status at a glance and lets you create or join shares without opening a terminal.
The installer detects GNOME automatically and offers to install it. To install or reinstall it manually:
~/.local/share/peerdup/gnome-extension/install.shThe top bar shows:
| State | Meaning |
|---|---|
| Idle | Daemon running, all shares up to date |
| Syncing | Active transfer in progress (rates shown inline: ↑1.2M ↓4.8M) |
| Error | Daemon unavailable or a share has an error |
The popup menu shows per-share status (peers, progress, transfer rates) and a
registry health indicator (green dot = connected and healthy, yellow = degraded,
red = unreachable). If zenity is installed, the menu also offers New
share... and Join share... dialogs.
Requires GNOME Shell 45+ and the peerdup daemon running on the same user account.
peerdup-setup --uninstallThis stops and disables the systemd daemon service, removes the GNOME Shell
extension (if installed), uninstalls all peerdup pip packages, and removes the
peerdup-setup symlink. It prompts before deleting your identity key, database,
and config so you can choose to preserve them.
To also remove the registry from a server machine:
peerdup-registry-setup --uninstallIf you prefer containers with automatic TLS (Let's Encrypt), you can run the
registry and relay via Docker Compose instead of peerdup-registry-setup.
You need a Linux host with a public IP, a DNS A record, and Docker CE.
git clone https://github.com/theronconrey/peerdup
cd peerdup
./start.shPrompts for your domain and email, starts the stack, and obtains a Let's Encrypt certificate via Caddy automatically. Subsequent runs skip the prompts and restart the stack.