Teams and remote workers who need to share files between devices on the same network — or across the internet — routinely reach for consumer tools that log transfers, impose size limits, or require accounts. PairDrop is a privacy-first, open-source alternative to AirDrop and Snapdrop that runs entirely in the browser. It uses WebRTC for peer-to-peer transfers and WebSockets for signalling, meaning files flow directly between devices with no server-side storage. Self-hosting it on your own Ubuntu server with Docker Compose and Caddy gives your team a zero-dependency, zero-account file drop that works on any device with a modern browser.
This guide walks through a production-ready PairDrop deployment: reverse-proxied behind Caddy for automatic HTTPS, persistent configuration, optional network pairing codes for cross-subnet transfers, and a Systemd override so the stack survives reboots. You will finish with a fully functional PairDrop instance your team can use immediately from any device on the network or over the internet.
Architecture and flow overview
PairDrop is a Node.js application that runs a lightweight signalling server. Browsers connect to this server over WebSocket, discover other peers on the same room or pairing code, then establish direct WebRTC data channels for the actual file transfer. The server never sees file bytes — it only brokers peer discovery.
In this stack:
- PairDrop container — the Node.js signalling server, exposed only on a local port (
127.0.0.1:3000). - Caddy — terminates TLS with an automatic Let's Encrypt certificate, reverse-proxies HTTP/WebSocket traffic to PairDrop, and handles HTTPS redirects.
- Docker Compose — orchestrates both containers with shared networking, restart policies, and volume mounts.
PairDrop rooms are scoped by IP subnet by default: two devices on the same LAN will automatically see each other. For cross-subnet pairing (VPN users, remote team members), PairDrop supports temporary pairing codes — a short code one device displays that another enters to join the same room. No accounts, no stored session data.
Caddy is ideal here because it handles WebSocket upgrades transparently and terminates TLS without manual certificate management. The reverse_proxy directive proxies both HTTP and WebSocket connections on the same listener.
Prerequisites
- Ubuntu 22.04 or 24.04 server with a public IP or reachable hostname.
- Docker Engine 24+ and Docker Compose v2 installed (
docker composecommand available). - A DNS A record pointing your desired domain (e.g.,
drop.example.com) to your server IP. - Ports 80 and 443 open in UFW and any upstream firewall.
- Root or sudo access on the server.
Step-by-step deployment
1. Create the project directory
mkdir -p /opt/pairdrop && cd /opt/pairdrop2. Create the environment file
Store configuration in a .env file and restrict its permissions immediately. The Compose file reads these variables via substitution so no configuration drifts into version-controlled files.
cat > /opt/pairdrop/.env << 'EOF'
PAIRDROP_DOMAIN=drop.example.com
PAIRDROP_PORT=3000
NODE_ENV=production
EOF
chmod 600 /opt/pairdrop/.envReplace drop.example.com with your actual domain name.
3. Write the Docker Compose file
version: "3.8"
services:
pairdrop:
image: lscr.io/linuxserver/pairdrop:latest
container_name: pairdrop
env_file: .env
environment:
- PUID=1000
- PGID=1000
- TZ=UTC
- RATE_LIMIT=false
- WS_FALLBACK=false
- RTC_CONFIG=
- DEBUG_MODE=false
ports:
- "127.0.0.1:${PAIRDROP_PORT}:3000"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
networks:
default:
name: pairdrop_net4. Write the Caddyfile
drop.example.com {
reverse_proxy 127.0.0.1:3000
encode gzip
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy interest-cohort=()
}
log {
output file /var/log/caddy/pairdrop.log
format json
}
}Replace drop.example.com with your actual domain. Caddy automatically obtains and renews a Let's Encrypt certificate on first startup. WebSocket connections work without additional configuration because Caddy's reverse_proxy directive transparently upgrades the connection.
5. Create the Caddy log directory and reload
mkdir -p /var/log/caddy
systemctl reload caddy 2>/dev/null || caddy reload --config /etc/caddy/Caddyfile 2>/dev/null || true6. Start the PairDrop stack
cd /opt/pairdrop
docker compose up -d
docker compose logs -f pairdropWait for the container to report Listening on port 3000 in its logs. Caddy will automatically request a certificate for your domain within a few seconds of the first incoming connection.
7. Enable auto-start on reboot
cat > /etc/systemd/system/pairdrop.service << 'EOF'
[Unit]
Description=PairDrop Docker Compose Stack
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/pairdrop
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable pairdropConfiguration and secrets handling
PairDrop exposes several runtime environment variables that control its security posture:
- RATE_LIMIT — set to
trueon public instances to throttle signalling requests per IP. Leavefalseon private internal servers. - WS_FALLBACK — set to
trueonly if you need WebSocket fallback over the signalling channel for clients that cannot establish WebRTC. Increases server-side load. - RTC_CONFIG — optional JSON string pointing to STUN/TURN servers for users behind strict NAT. Example:
'{"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]}'. For corporate environments with symmetric NAT, add a TURN server from Coturn or a cloud provider. - DEBUG_MODE — never enable in production; it exposes signalling internals in browser console.
Keep your .env file at chmod 600 and owned by root. If you extend the stack with TURN credentials in RTC_CONFIG, treat that JSON value as a secret and consider sourcing it from a secrets manager rather than storing it in the file system.
Verification
Run these checks after the stack is up:
# Confirm PairDrop container is healthy
docker compose -f /opt/pairdrop/docker-compose.yml ps
# Confirm local port is listening
curl -s http://127.0.0.1:3000 | head -c 200
# Confirm HTTPS endpoint is reachable
curl -sI https://drop.example.com | grep -E "HTTP|content-type"
# Confirm TLS certificate is valid
echo | openssl s_client -connect drop.example.com:443 2>/dev/null | openssl x509 -noout -datesOpen https://drop.example.com on two different devices connected to the same network. Both should appear in each other's device list within a few seconds. Drop a file from one device to the other — it should transfer directly via WebRTC with no server-side storage.
Common issues and fixes
Devices do not see each other: PairDrop groups peers by IP subnet. If your devices are on different subnets (e.g., one on Wi-Fi, one on wired Ethernet with different /24 blocks), use the pairing code feature — one device generates a code, the other enters it. This creates a shared room regardless of subnet.
WebRTC transfer fails / file never arrives: The signalling handshake succeeded but the WebRTC data channel cannot establish a direct path. This is a NAT traversal problem. Set RTC_CONFIG to include a public STUN server (e.g., stun:stun.l.google.com:19302). For users behind symmetric NAT (common in corporate environments), you must add a TURN server.
Caddy returns 502 Bad Gateway: The PairDrop container is not running or not listening on 127.0.0.1:3000. Check with docker compose -f /opt/pairdrop/docker-compose.yml ps and inspect logs with docker compose logs pairdrop.
Certificate not issued / HTTPS not working: Ensure your DNS A record points to the server's public IP and that port 80 is reachable from the internet (Caddy uses HTTP-01 challenge by default). Check journalctl -u caddy --since '5 min ago' for ACME errors.
Large files stall at browser level: WebRTC data channel chunk size limits are browser-dependent. Most modern browsers handle up to several GB, but memory-constrained mobile browsers may stall on files above 2–3 GB. Split large archives before transfer or use a different tool for multi-GB payloads.
FAQ
Does PairDrop store any files on the server?
No. The server only runs the WebSocket signalling service. File data flows peer-to-peer via WebRTC data channels directly between browsers. The server never sees, stores, or logs file contents.
Can users outside my network use PairDrop?
Yes. PairDrop's pairing code feature creates a temporary shared room identified by a short alphanumeric code. Share the code with a remote user — they enter it on the PairDrop interface at your domain and join the same room regardless of their network location. File transfer still uses WebRTC, so NAT traversal may require a STUN or TURN server for users behind strict firewalls.
How do I restrict PairDrop to internal users only?
The simplest approach is to not expose PairDrop's domain publicly — add it only to your internal DNS resolver and keep port 443 firewalled from the internet. Alternatively, put PairDrop behind Authentik or Authelia as a forward-auth middleware in Caddy, requiring a login before reaching the PairDrop interface.
What happens when I restart the server?
The systemd service created in Step 7 runs docker compose up -d on boot, so PairDrop restarts automatically after a reboot. Active peer sessions will be dropped during the restart window and clients will reconnect on their next page refresh.
Can I run PairDrop alongside other Docker services on the same Caddy instance?
Yes. Caddy supports multiple site blocks in a single Caddyfile. Add the PairDrop block alongside your existing service blocks. Each site gets its own TLS certificate and reverse proxy target. There is no port conflict because PairDrop binds to 127.0.0.1:3000 — change this if another service already uses port 3000.
Does PairDrop support sending to multiple recipients simultaneously?
PairDrop supports sending to multiple visible peers in the same room. Select multiple recipients in the interface and each will receive a separate WebRTC data channel for concurrent transfer. Transfer speed per recipient depends on the sender's upload bandwidth divided among all active channels.
How do I update PairDrop to a newer version?
Pull the latest image and recreate the container:
cd /opt/pairdrop
docker compose pull
docker compose up -d --remove-orphansThe LinuxServer.io image is rebuilt automatically when upstream PairDrop releases a new version. Run these commands weekly or automate with a Watchtower container if your policy permits automatic image updates.
Internal links
- Production Guide: Deploy Vaultwarden with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Uptime Kuma with Docker Compose + Caddy on Ubuntu
- Production Guide: Deploy Stirling PDF with Docker Compose + Caddy + OCR on Ubuntu
Talk to us
If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.