ConcordManual Home GitHub
Making it yours
On this pageThe easy answer: TailscaleRun one locally (to try it, or on a LAN)Run one with DockerRun one on a VPSRun one on fly.ioHanding the address outOptionsWhat running one commits you toIf it is not working

Running a rendezvous

How to decide whether your group needs a rendezvous node, and how to run one locally, with Docker, on a VPS or on fly.io.

Two Concord peers can only talk if at least one of them can accept an incoming connection. On the same Wi-Fi that is free. Across the internet, home routers usually refuse, and something has to introduce the two sides.

That something is a rendezvous: a small always-on node that helps peers find each other and forwards their traffic when neither can be dialled directly. It is not a server in the sense Concord is arguing against — it holds no accounts, stores no history, and cannot read a message. §6 of DESIGN.md sets out exactly what it can and cannot see.

You may not need one. Work down this list and stop at the first that fits.

Your situationWhat to doCost
Everyone on the same Wi-FiNothing. Peers find each other over mDNS.Desktop-to-desktop only; Android's sandbox blocks the socket, and many work and campus networks drop the packets.
You already use Tailscale or WireGuardNothing. See below.Everyone needs to be on the same tailnet.
One person can forward a portThat person pins a port and adds one router rule. Invite codes carry their address.Their home IP is inside every invite code they hand out.
Nobody can forward a portSomebody runs a rendezvous. The rest of this document.One machine has to exist somewhere. It does not have to be yours.

Inside the app, Can people reach me? (in the Ctrl+K palette, and beside every invite code) tells you which row you are actually in.

The easy answer: Tailscale

If you and your friends are on a Tailscale tailnet — or any WireGuard mesh — you need no rendezvous at all. Every machine gets a stable address that every other machine can dial directly, which is precisely the thing home routers refuse to provide.

Start Concord normally on each machine; there is nothing to configure. Concord listens on every network interface, the tailnet one included, and invite codes carry this machine's network addresses, so a code handed to someone on the same tailnet carries an address they can dial.

This is the best option for a private group: no public infrastructure, no port-forwarding, and no IP address handed to strangers. It does not help you meet someone who is not on your tailnet — for that, read on.

Run one locally (to try it, or on a LAN)

make rendezvous          # builds bin/rendezvous
./bin/rendezvous

It prints its PeerID and its bootstrap addresses. Point a peer at one:

CONCORD_BOOTSTRAP="/ip4/192.168.1.50/tcp/4001/p2p/12D3KooW…" ./bin/concord-web

or paste the same address into Settings → Connection → Rendezvous server in the running app, which saves it and dials immediately.

By default the node's identity is random, so its PeerID — and therefore its address — changes on every restart. Pin it:

CONCORD_RELAY_SEED=$(openssl rand -hex 32) ./bin/rendezvous

Keep that seed. It is the node's identity; losing it invalidates every bootstrap address you have handed out.

A two- or three-node mesh on one machine is not a representative test of DHT discovery: the routing table is too small to converge the way it does in the open. Local runs are good for checking the node starts and peers connect to it, not for judging whether discovery works.

Run one with Docker

The image builds only the node — no frontend, no app.

docker build -f infra/rendezvous/Dockerfile -t concord-rendezvous .
docker run -d --name rendezvous --restart unless-stopped \
  -p 4001:4001/tcp -p 4001:4001/udp \
  -e CONCORD_RELAY_SEED=$(openssl rand -hex 32) \
  concord-rendezvous
docker logs rendezvous     # read the printed bootstrap address

Both TCP and UDP on the same port: libp2p uses TCP and QUIC, and losing UDP costs you the faster path and some NAT traversal.

Put the seed in a file rather than the command line if the host is shared — it is the node's private identity, and docker inspect shows environment variables.

Run one on a VPS

Any small machine with a public IP works; the node is a single static binary with no dependencies. The cheapest tier at any provider is enough — it forwards ciphertext and answers DHT queries, and 256 MB of RAM is plenty for a group of friends.

# on the server
git clone https://github.com/ZahakJ/concord && cd concord
make rendezvous
sudo cp bin/rendezvous /usr/local/bin/concord-rendezvous

Give it a stable identity and a service that restarts it:

sudo tee /etc/concord-rendezvous.env >/dev/null <<EOF
CONCORD_RELAY_SEED=$(openssl rand -hex 32)
PORT=4001
EOF
sudo chmod 600 /etc/concord-rendezvous.env

sudo tee /etc/systemd/system/concord-rendezvous.service >/dev/null <<'EOF'
[Unit]
Description=Concord rendezvous node
After=network-online.target

[Service]
EnvironmentFile=/etc/concord-rendezvous.env
ExecStart=/usr/local/bin/concord-rendezvous
Restart=always
RestartSec=5
DynamicUser=yes
# It needs nothing from the filesystem but its own binary.
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
NoNewPrivileges=yes

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable --now concord-rendezvous
sudo journalctl -u concord-rendezvous -n 30    # read the bootstrap address

Open 4001 on both TCP and UDP in the provider's firewall and in ufw:

sudo ufw allow 4001/tcp && sudo ufw allow 4001/udp

If the machine has a DNS name, set CONCORD_PUBLIC_HOST to it and the node prints a /dns/… address instead of a raw IP. That address survives the machine being rebuilt on a new IP, which a /ip4/… one does not — worth doing before you hand the address to anyone.

Run one on fly.io

fly.rendezvous.toml in the repository root is a working deployment on a free account. Rename the app first — fly app names are globally unique.

fly launch --no-deploy -c fly.rendezvous.toml
fly secrets set CONCORD_RELAY_SEED=$(openssl rand -hex 32)
fly secrets set CONCORD_PUBLIC_HOST=<your-app>.fly.dev
fly deploy -c fly.rendezvous.toml
fly logs                      # read the printed /dns/… address

Do not enable the TURN relay on fly.io. The config ships with that block commented out and a comment explaining why: fly's edge does not hairpin UDP back to the advertised public IP, so a relay there authenticates, allocates, and then carries nothing, while calls silently fail instead of falling back. Host the relay on a machine that owns its public IP if you want it.

Handing the address out

The node prints something like:

/dns/rdv.example.com/tcp/4001/p2p/12D3KooWCzonwxmETSLwgDY9JAe9WczUHssnYTrSpCqkp6UXZg1q

Friends paste that into Settings → Connection → Rendezvous server, or start with CONCORD_BOOTSTRAP=… (comma-separated for several). After that it is automatic: an invite code generated by someone bootstrapped through your node carries the node's address with it, so the person joining is pointed at it without being told to configure anything.

Options

Everything is an environment variable, and none is required.

VariableDefaultWhat it does
PORT4001libp2p listen port, TCP and UDP.
CONCORD_RELAY_SEEDrandom32-byte hex seed fixing the node's identity. Set it, keep it.
CONCORD_PUBLIC_HOST—DNS name to advertise, so the printed address survives an IP change.
CONCORD_GUEST_PORT8080Plain-HTTP port of the gateway for browser guests joining a meeting with no install, and for the TURN credentials. It always runs. Guest links point at https:// plus the DNS name in the rendezvous address, so something must terminate TLS on port 443 of that name and forward here.
CONCORD_GIF_KEYoffEnables GIF search through this node. Without it the picker says the feature is unavailable.
CONCORD_GIF_PROVIDERgiphygiphy or tenor. Tenor's public API was decommissioned in June 2026; it is kept for self-hosted mirrors.
CONCORD_GIF_BASEthe provider's APIAPI base URL, for a self-hosted mirror.
CONCORD_GIF_CONTENTFILTERstrictestContent rating passed to the provider (for Giphy: y, g, pg, pg-13, r; default g).
CONCORD_TURN_SECREToffEnables the TURN relay for IP-private calls. Read the fly.io warning above first.
CONCORD_TURN_PORT3478TURN listen port when the relay is enabled. TCP only.
CONCORD_TURN_RELAY_IPresolved from CONCORD_PUBLIC_HOSTThe machine's own public IP for the relay to advertise. Without a routable address the relay refuses to start and calls connect directly.
CONCORD_PUSH_TOKENSpush-tokens.jsonWhere push-wake tokens are kept, when push credentials are configured. See PUSH.md.

The legacy CONCORD_TENOR_KEY, CONCORD_TENOR_BASE and CONCORD_TENOR_CONTENTFILTER still work and select Tenor.

What running one commits you to

  • It sees who talks to whom, and when. It cannot read a message, a name, or a file — those are encrypted end to end and it holds no keys — but it necessarily learns which peers connect through it and at what times. Run one for people who are content for you to know that.
  • It uses very little. Bandwidth is only what it relays, and it relays only for peers that cannot connect directly.
  • It is not a backup. No history lives there. Its mailbox holds encrypted messages for offline devices, in memory only, for at most 14 days, and loses them on restart. If everyone's devices are lost, the conversation is gone; the node has nothing to restore from.
  • It going away is survivable. Peers remember addresses they have reached before and re-dial them directly, so an outage degrades discovery rather than stopping the app.

If it is not working

SymptomLikely cause
Peers never see each other, node looks healthyUDP is blocked, or only TCP is forwarded. Open both on the same port.
The address stopped working after a restartNo CONCORD_RELAY_SEED, so the identity — and the PeerID in the address — was regenerated.
The address stopped working after a rebuildIt was an /ip4/… address and the IP changed. Set CONCORD_PUBLIC_HOST and hand out the /dns/… form.
Works on the LAN, not across the internetmDNS is doing the work locally. Set CONCORD_DISABLE_MDNS=1 on a peer to test the real path.
Calls connect but nobody hears anythingA TURN relay that cannot hairpin — see the fly.io note. Disable it and calls fall back to direct.
Guest meeting links do nothingThe rendezvous address is an /ip4/… one, so there is no name to build the link from, or nothing terminates TLS on port 443 and forwards to CONCORD_GUEST_PORT (default 8080).
"Hide my IP on calls" says this rendezvous has no media relayCONCORD_TURN_SECRET is unset; or the relay refused to start for lack of a routable address (it says so on stderr; set CONCORD_TURN_RELAY_IP); or peers cannot fetch relay credentials, which come from the guest gateway at https://<name>/turn and so need a /dns/… address and TLS in front, as for guest links.

Edit this page on GitHub

Concord is free software under the GNU Affero General Public License, version 3 or later. These pages are built from the Markdown in the repository.