Self-hosted, E2E encrypted file transfer between a phone and any browser, via QR or PAKE code
608
Self-hosted, end-to-end encrypted file transfer between a phone and any browser — even across different networks, with no native app, no account, and no file ever touched by the server. Pair with a QR code (devices physically together) or a short spoken code (remote devices), then send.
docker run -d --name quicksend -p 8080:8080 marekotulakowski/quicksend:latest
Open http://localhost:8080.
Note: browsers only allow the encryption Quicksend needs (Web Crypto) over HTTPS or on localhost itself — plain HTTP to any other address (e.g. a LAN IP) lets you look around the page but not actually pair or transfer. See "Production Deployment" below for real HTTPS.
services:
quicksend:
image: marekotulakowski/quicksend:latest
restart: unless-stopped
ports:
- "8080:8080"
docker compose up -d
Same localhost-only limitation as above — fine for a quick look, not for pairing with a phone over the network.
This runs the relay behind Caddy as a reverse proxy, with Caddy getting its certificate from Let's Encrypt via a Cloudflare DNS-01 challenge — only port 443 needs to be open on your firewall, never port 80. Requires your domain's DNS to be managed by Cloudflare.
curl -fsSL https://get.docker.com | sh
No need to clone the whole repository — just these four:
mkdir -p quicksend/caddy && cd quicksend
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/MarekOtulakowski/Quicksend/main/docker-compose.yml
curl -fsSL -o Caddyfile https://raw.githubusercontent.com/MarekOtulakowski/Quicksend/main/Caddyfile
curl -fsSL -o caddy/Dockerfile https://raw.githubusercontent.com/MarekOtulakowski/Quicksend/main/caddy/Dockerfile
curl -fsSL -o .env.example https://raw.githubusercontent.com/MarekOtulakowski/Quicksend/main/.env.example
In Cloudflare's DNS settings, add (or edit) an A record for your domain pointing at this server's public IP, with the proxy status set to DNS only (grey cloud) — not Proxied. Proxying it isn't wrong exactly, but it puts Cloudflare's own caching and protocol negotiation in front of what's otherwise a single long-lived WebSocket per session, and it's not needed for anything here: the DNS-01 challenge below only ever talks to Cloudflare's API, never the proxied traffic path.
cp .env.example .env
Edit .env:
QUICKSEND_DOMAIN — the domain from step 3CLOUDFLARE_API_TOKEN — a token scoped to just that zone with "Zone / DNS / Edit" permission (create one at dash.cloudflare.com/profile/api-tokens, the "Edit zone DNS" template) — never your account's Global API Key, which can edit every zoneIf the host has a firewall enabled (e.g. ufw), allow 443 — port 80 is never needed:
sudo ufw allow 443/tcp # skip if you don't use ufw, or 443 is already open
docker compose up -d
Open https://<QUICKSEND_DOMAIN> — Caddy issues the certificate automatically on first request.
All limits are environment variables on the quicksend service:
| Variable | Default | Meaning |
|---|---|---|
QUICKSEND_LISTEN_ADDR | :8080 | Address the relay binds to |
QUICKSEND_MAX_FILE_SIZE_BYTES | 10 GiB | Max size of a single file the relay will keep relaying |
QUICKSEND_MAX_SESSIONS_PER_IP | 10 | Concurrent sessions allowed per client IP |
QUICKSEND_SESSION_INACTIVITY_TIMEOUT | 5m | Idle session teardown |
QUICKSEND_RECONNECT_GRACE_PERIOD | 45s | How long a session survives a dropped connection |
QUICKSEND_PAIRING_CODE_TTL | 5m | How long a remote pairing code stays valid |
QUICKSEND_MAX_PAIRING_ATTEMPTS | 5 | Wrong pairing-code guesses allowed per IP before throttling |
linux/amd64 + linux/arm64.Content type
Image
Digest
sha256:0fb3d905f…
Size
7.1 MB
Last updated
24 days ago
docker pull marekotulakowski/quicksend