Hosting a server
One person runs the server; everyone else connects to it. It is a single program on one UDP port: no website, no domain name and no certificate to set up.
What you need
- A computer your friends can reach, left on while you talk: a spare PC at home, a small VPS, a homelab box.
- UDP port 51000 open to it. Everything (voice, video, chat, logging in) uses that one port. At home, forward it on your router to that computer; on a VPS, allow it in the provider's firewall.
-
Its public IP address. People type the address
and port (
203.0.113.5:51000); host names are not supported yet.
Every connection is encrypted (QUIC over UDP) and checked against the server's own key, which it makes on first start, so there is no certificate to buy or renew.
From the download
Every desktop download (Windows, macOS, Linux) contains the app
and the server, dedicated-server.
-
Download the zip for the computer that
will host, and unzip it into a folder you will keep. The server
writes its files into the folder it is started from, so start it
from there: on Windows, double-click it; on macOS and Linux,
cdinto the folder and run./dedicated-server. -
Start it (on Windows it is
dedicated-server.exe; allow it through the firewall when Windows asks). On first start it writesserver.yamlwith the defaults and createsdata/baucord/, where its key and database live. -
Open UDP port 51000 (see above). To use another port, change
portinserver.yamland restart. -
Look at what it printed. Two lines matter:
SetAddress for clients: <public-ip>:51000 — identity SHA256:… No admin yet. Bootstrap invite (admin, 1 use, 1 h): code K7FQ-2MX9-ABCD for …public_addressinserver.yamlto your public IP and port, and the first line will show it. The second is your way in; the next section says what to do with it. - Keep it running: a terminal window left open is enough, or your system's service manager. Stopping it (Ctrl+C) tells everyone connected that the server is restarting.
Becoming admin, inviting people
A device's key is its account: nobody has a password. A device the server does not know yet needs an invite code once; after that it just connects.
- You first. While the server has no admin, it prints a one-time admin code at every start (good for one use, for an hour). In your app: Settings → Servers, add the server's address, paste the code into Invite code, and connect. You are now its admin.
-
Everyone else. Type
/invitein the chat box, or open the admin panel. The code is copied for you; send it together with the address and port. Options:/invite [guest|member|organizer] [24h|7d] [uses]. -
Optional: the identity in the
Address for clientsline can go into Settings → Servers too, so the app checks it is talking to your server before it sends anything. Without it, the app learns the identity on the first connection.
Roles go from guest to member, organizer and admin, and you can make your own. A code says which role it gives; change people's roles later from the admin panel.
On a Linux host with Docker
Docker Compose on a Linux VPS, with the published image
mpwsh/baucord:latest, updated with every release. It
is distroless and runs as uid 10000. Everything you
need is on this page: four small files and the image, which
Docker downloads by itself.
The image, like the Linux download, is for x86-64 machines. ARM hosts (a Raspberry Pi, an ARM VPS) are not supported yet.
client ──udp/51000 (QUIC)─▶ baucord (published directly)
One service. Media leaves straight from the container, not through a userspace proxy, which would add copies per packet and hide the real client addresses from the server.
The files
Four files, all downloadable from here. Read a script before you run it as root.
| File | Goes to | What it is |
|---|---|---|
| docker-compose.yaml | docker-compose.yaml |
The one service: image, port, volumes, log rotation |
| baucord.yaml | config/baucord.yaml |
The server's configuration, to edit (Configuration) |
| setup.sh | scripts/setup.sh |
Sets up the host and starts the server; safe to run again |
| monitor-net.sh | scripts/monitor-net.sh |
Logs where UDP packets get dropped (Monitoring) |
The compose file:
# One service: the relay. Everything — media, chat, admission — rides
# one QUIC port over UDP. config/baucord.yaml is this host's own config
# (start from https://baucord.com/deploy/baucord.yaml); ./data/baucord holds the
# server's identity key and database: back it up. The guide, and this
# file to download: https://baucord.com/deploy.html
services:
baucord:
container_name: baucord
# Published on every release (x86-64). Update: docker compose pull,
# then docker compose up -d (or run scripts/setup.sh again).
image: mpwsh/baucord:latest
restart: always
ulimits:
nofile:
soft: 65536
hard: 65536
ports:
# The same number as server.port in config/baucord.yaml. Moving a
# server off another port: publish both while saved addresses
# catch up, e.g. also "5000:51000/udp".
- "51000:51000/udp"
volumes:
- ./config/baucord.yaml:/app/server.yaml
- ./data/baucord:/app/data
# The server logs to stderr here, so this is its log: kept to
# 5 × 10 MB instead of growing for ever (it names people and IPs).
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
networks:
baucord:
networks:
baucord:
Next to them, the server creates data/baucord/: its
identity key, database, shared files and the logs people send.
Back it up.
Setting up a host
-
Allow
udp/51000in the provider's firewall. Nothing else: no DNS record, no TCP port, no certificate. -
Put the files in a folder of their own, for example
/opt/baucord:sudo mkdir -p /opt/baucord/config /opt/baucord/scripts cd /opt/baucord B=https://baucord.com/deploy sudo curl -fsSL -o docker-compose.yaml $B/docker-compose.yaml sudo curl -fsSL -o config/baucord.yaml $B/baucord.yaml sudo curl -fsSL -o scripts/setup.sh $B/setup.sh sudo curl -fsSL -o scripts/monitor-net.sh $B/monitor-net.sh sudo chmod +x scripts/*.sh -
In
config/baucord.yaml, setserver.public_addressto the host's public IP and port, and the server's name and channels to taste (Configuration). -
Run the setup script:
It is safe to run again after any change. It:sudo scripts/setup.sh- installs Docker and the compose plugin from the distribution's packages, only if they are missing;
-
creates
./data/baucordowned by uid 10000, where the server makes its identity key on first start; -
tunes the host for UDP fan-out: socket buffer limits and the
kernel's receive backlog
(
/etc/sysctl.d/99-baucord-udp.conf), connection tracking sized for Docker's port publishing, NIC ring buffers and RPS at boot, and no cloud-init network hotplug; -
pulls the image and starts the server
(
docker compose pull,docker compose up -d); - prints the server's address and identity, and on a new server the one-time admin invite code;
- installs the network monitor as a service (Monitoring).
MEDIA_VIA_DOCKER_NAT; the uid; the network interface, detected) are the only things you should ever need to touch. -
Check that it came up:
The address and the admin code are in the log too:sudo docker compose ps sudo docker compose logs -f baucord ss -uln | grep :51000sudo docker compose logs baucord | grep -E 'Address for clients|Bootstrap invite'
Host networking (optional)
For the last bit of headroom, run the service with
network_mode: host instead of ports:.
That takes Docker's address translation and connection tracking
out of the media path. Then set
MEDIA_VIA_DOCKER_NAT="false" in
scripts/setup.sh and run it again: it adds a
notrack rule for the media port, which is only safe
without Docker's translation.
Configuration
One YAML file: server.yaml next to the server from
the download, config/baucord.yaml with Docker. The
parts you will touch:
server:
name: My baucord server
port: 51000 # UDP
public_address: "203.0.113.5:51000" # printed for people to type
greeting_message: Welcome!
identity:
dir: data/baucord # the identity key: back it up
logging:
file: server.log # "" = stderr only (Docker)
link_stats_interval_secs: 0 # per-client link: lines (2 while diagnosing)
telemetry: true # log clients' telemetry: samples
persistence:
history: { retain_days: 90 } # chat and direct messages
files: { retain_days: 30 } # files nothing uses any more
telemetry: { retain_days: 30 } # clients' 10-second samples
channels:
- name: General
- name: Gaming
password: "hunter2" # optional
user_limit: 8 # optional
-
Channels apply as soon as the file is saved,
while the server runs; a bad edit is logged and ignored.
Everything else needs a restart. Leave out
channelsfor a single open General. The lobby everyone arrives in is added by the server and is not in this list. A channel is known by its name: renaming one moves the people in it to the lobby. - A missing file is written with the defaults; a file that does not parse stops the server rather than being replaced.
-
--config,--hostand--porton the command line override the file. -
The
authenticationsection of 1.0 (a shared room password) is ignored since 1.1, with a warning: invites are the only way in.
Managing from the command line
Most things are in the app's admin panel. The same tools work from
the server's command line, also while it runs (it picks changes up
within two seconds). From the download:
./dedicated-server <command>. With Docker:
cd /opt/baucord
sudo docker compose exec baucord /app/server <command>
| Command | What it does |
|---|---|
invite create --grants member --expires 7d --uses 5 |
A new code (role: guest by default; expires: 24h by
default, or never; uses: unlimited by default)
|
invite list, invite revoke CODE |
Every code with its state; stop one admitting anyone else |
user list |
Everyone with their roles, keys and when they were last seen |
user grant NAME admin, user revoke NAME ROLE |
Add or remove a role (the last admin keeps admin) |
user kick NAME,
user ban NAME --for 7d,
user mute NAME
|
Kick (they need a new invite), ban (their device keys; a
ban from the app also blocks their address while they are
online), mute (their voice is dropped); all take
--reason
|
user purge NAME |
Delete a person: account, keys, roles and messages |
role list, role create … |
Custom roles: named sets of permissions |
backup FILE.tar, restore FILE.tar |
See backups |
rotate-key |
A new identity key (with the server stopped) |
db check, db prune |
Check the database; run the retention clean-up now |
--help after any command lists all of its options.
The identity key and backups
On first start the server makes an identity key in
data/baucord/. Everyone's app remembers it: lose it
and every app will refuse the server as an impostor. Keep a copy
of the whole directory: the key, the database (accounts, roles,
invites, messages) and the files people shared.
A consistent backup, safe while the server runs:
# from the download
./dedicated-server backup baucord-2026-10-08.tar
# with Docker (the file lands in ./data/baucord)
sudo docker compose exec baucord /app/server backup /app/data/baucord-2026-10-08.tar
To put one back, stop the server and run
restore FILE.tar (with Docker:
sudo docker compose stop baucord, then
sudo docker compose run --rm baucord restore /app/data/FILE.tar).
The database it replaces is kept beside it.
To change the key (if it may have leaked), stop the server and run
rotate-key (with Docker:
sudo docker compose stop baucord,
sudo docker compose run --rm baucord rotate-key,
sudo docker compose start baucord). The new key is vouched
for by the old one, so apps move to it on their own.
Updating
-
From the download: stop the server, unzip the
new version over the old one (your
server.yamlanddata/stay), start it again. -
With Docker: from the server's folder,
or runsudo docker compose pull sudo docker compose up -dsudo scripts/setup.shagain, which does both. The compose file and the scripts change rarely; when they do, download them again, and keep yourconfig/baucord.yaml.
A restart is a blink: the server tells every app it is restarting, and they reconnect on their own about two seconds later. Keep the server and the apps on the same version: when the connection format changes, an older app is turned away with a message to update rather than failing halfway through a call.
Reading the server log
From the download it is server.log (and the
terminal); with Docker, sudo docker compose logs baucord
in the server's folder.
Two periodic lines carry most of what you will ever need:
-
link: <user>: rtt, cwnd, path loss, tx/rx, mtu, dgram drops, groups behind N/M (cause), voice rx/expected/lost/late: the server's view of one person's connection, everylink_stats_interval_secs. Path loss is the transport's own count and only informative; voice lost is what listeners actually missed; groups behind is how often that person's download could not keep up with a screen share, and the cause says which rule gave up. -
telemetry: <user>: …: the app's own numbers every 10 seconds: connection, audio quality steps, the encoder's step and frame rate while sharing, decoding while watching.
Rising loss with a flat round-trip time is that person's network; a rising round-trip time with little loss is a queue somewhere, the server's or a router's.
Monitoring
A server is a UDP fan-out: a ~177 KB keyframe goes to every viewer
at once, and the kernel drops such bursts silently. What you see
is abandoned video groups and timeouts, not errors. On a Docker
host, setup.sh runs scripts/monitor-net.sh
as a service, baucord-monitor, which writes a CSV row
every 10 seconds to /var/log/baucord-net.log (rotated
weekly, about two months kept):
systemctl status baucord-monitor
tail -f /var/log/baucord-net.log
UDP counters are per network namespace, so it samples the host
and the baucord container, one row each.
Every *_err and *_drop column is the
change since the last row, and note names the layer
that dropped:
| note | meaning |
|---|---|
RCVBUF |
Receive buffer overflow: the server is not draining it, or rmem is small |
SNDBUF |
Send buffer overflow: a keyframe burst bigger than wmem, or the link is full |
SOFTNET |
Kernel backlog overflow: netdev_max_backlog, or out of CPU |
NIC |
The NIC's ring, or the provider's packet or bandwidth allowance: see ethtool -S |
VETH |
Drops on Docker's bridge |
COUNTER_RESET |
The container restarted |
max_txq_bytes creeping towards wmem_max
during keyframes means SNDBUF drops are close: pace
keyframes rather than raising the cap. softirq_pct
above about 30 % for long means packet processing is out of CPU.
ct_count nearing ct_max means connection
tracking is about to start refusing new people.
Your server, your data
Whoever runs a server holds what people send through it, and
answers for it. The server stores accounts, roles, chat and direct
messages, shared files and the apps' call-quality samples in
data/baucord/, for as long as the
persistence settings say; logs people send from
their app land in data/baucord/logs/. Its own log
records connections with IP addresses and the text of chat
messages. Connections are encrypted but not end to end: the server
can read what passes through it.
Tell the people on your server what you keep and for how long. The privacy policy of the official server lists everything a server receives and is a fair starting point.