Tephro · Guides
Running a Tephro server
One binary, one directory, one port. This is the whole procedure; if a step here does not work, that is a bug worth reporting.
1. Get the binary
Download the build for your platform from the releases page (five targets: Linux amd64/arm64, Windows amd64, macOS amd64/arm64), or build it yourself with Go 1.26+:
git clone https://gitlab.com/AuHunt/tephro cd tephro && make server-build # → dist/server/tephro
Check the download: every release ships SHA256SUMS and its OpenPGP signature SHA256SUMS.asc, made with the maintainer's signing key on a hardware token. With gpg installed:
curl -fsSL https://gitlab.com/AuHunt.gpg | gpg --import gpg --verify SHA256SUMS.asc SHA256SUMS sha256sum -c SHA256SUMS
The second line must report a good signature from a key with the primary fingerprint 3174 0E3B 30C6 597B 5578 FE09 A760 51A5 9D84 E725; the third proves the binary matches the checksums. The same key is embedded in the desktop client, which runs the same two checks before installing an update. Nothing else is needed on the box: no database server, no reverse proxy, no TURN daemon.
2. Run it once
./tephro
In a terminal, the first run is a short setup wizard: instance name, listen address, TLS mode, admission mode, whether to mint the owner invite, your public IP (detected with one request to Mullvad's echo service, typed, or skipped), and which address the owner invite link should carry — the public one, this machine (127.0.0.1, when the client runs on the same box), or one you type. **On Linux, as an ordinary user, the very first thing you may see instead is permission denied on port 443** — that is section 3, one command, and the wizard walks you through it when there is a terminal. It writes tephro.toml into the data directory, prints the owner invite, and then the server goes to the background and your shell comes back with the process id and the log path. tephro tui shows the live status view whenever you want it; tephro admin shutdown stops the server. The data directory is the one place the server writes: by default ~/.local/share/tephro/server on Linux (%LOCALAPPDATA%\Tephro\server on Windows, ~/Library/Application Support/Tephro/server on macOS — the desktop client keeps its own files beside it under the same root), or wherever --data-dir / TEPHRO_DATA_DIR points; TEPHRO_HOME moves the whole root. Run the binary from any directory and it finds the same instance. Every answer is also a config key, so a headless box (a container, a service manager) needs no wizard: it boots on defaults and logs what to change.
The one thing to know: the owner is whoever registers with the owner invite. Mint it in the wizard or later with
./tephro admin invite --owner
and register with that key from the desktop client. If the owner disappears — the person is unreachable, their machine is gone — mint a replacement from the box: tephro admin invite --owner --force. Whoever registers with it becomes the owner and the previous owner loses the flag (the audit log records it). Without --force the command refuses while an active owner with keys exists and points at tephro admin owner <username>, which moves ownership to an existing account instead. Every later invite comes from the client, or from tephro admin invite: a plain one is a personal key (one use, admits directly); --public makes a public link (unlimited uses) whose joiners wait for your approval unless you pass --approval no or turn the public_invite_approval setting off. tephro admin approvals lists who is waiting; approve <name> and deny <name> decide. Every invite is printed with a second, same machine link for a client running on this box.
3. Ports
The server listens on TCP 443 (HTTPS, the client's websocket, and the ICE-TCP fallback) and UDP 443 (all voice and video, every call, one port). Nothing else. Members' networks are least likely to block 443, which is why it is the default and why you should keep it if you can.
Linux needs one grant to bind a port below 1024. Pick whichever fits how you run it:
| How you run it | What to do |
|---|---|
| By hand | sudo setcap cap_net_bind_service=+ep ./tephro — repeat after replacing the binary |
| systemd | Use server/deploy/tephro.service; it carries AmbientCapabilities=CAP_NET_BIND_SERVICE |
| Container | Publish the port (-p 443:443 -p 443:443/udp). Rootful docker or podman: nothing else. Rootless podman refuses ports below 1024: sudo sysctl net.ipv4.ip_unprivileged_port_start=443 (and a file in /etc/sysctl.d/ to keep it), or publish a high host port (-p 8443:443 -p 8443:443/udp) and have the router forward external 443 to it — the server inside still binds 443, so this is the one place a port translation works |
| Behind a home router | Grant the bind as above and forward external 443 → 443, TCP and UDP, to this machine. Set public_ip — the wizard detects it, or run tephro admin public-ip. Details below |
The wizard test-binds your chosen port and walks through these on a permission error. Windows and macOS do not restrict low ports.
Behind a home router
The port must be the same on both sides of the router. The server advertises the port its socket is bound to, and the client aims at the port in the invite link, so a rule that translates external 443 to an internal 8443 leaves every call knocking on a port nobody forwarded. Two shapes work:
- 443 both sides (recommended): grant the bind (the table above),
listen = "0.0.0.0:443", router forwards TCP 443 and UDP 443 to this machine. Members reach the least-blocked port. - A high port both sides:
listen = "0.0.0.0:8443", router forwards TCP 8443 and UDP 8443. No grant; the invite link carries:8443; members on restrictive networks may not reach it.
Then public_ip. The wizard's detect asks an echo service once and writes the literal address; tephro admin public-ip does the same later. Without it the server's offers name only its LAN address and calls never find the way in. Home addresses change: when yours does, run it again and hand out fresh links, or point a DNS name here and set domain. Members on your own LAN get a link with the LAN address — tephro admin invite --host 192.168.1.10 — because most home routers do not route a LAN client out and back in.
Before inviting anyone, open https://<public_ip> from a phone on mobile data. The certificate warning is expected (self-signed); the page behind it is the instance's own. If nothing loads, check the forward rule and this machine's firewall, then whether your ISP puts you behind carrier-grade NAT: the router's WAN address is in 100.64.0.0/10 or differs from the detected public IP. Behind CGNAT no forward works and a VPS is the answer.
4. TLS and the fingerprint
By default the server generates a self-signed certificate on first run and prints its fingerprint. The desktop client pins that fingerprint — it does not need a certificate authority, so a bare IP address works. The fingerprint travels inside every invite link (tephro://join?host=…&fp=sha256:…&key=…), so a member's first connection is verified rather than trusted.
If you have a DNS name and port 443 reachable from the internet, tls_mode = "acme" with domain = "chat.example.org" gets a Let's Encrypt certificate automatically. If a reverse proxy terminates TLS in front, tls_mode = "off" and trusted_proxies set to the proxy's address, so rate limits and logs see your members rather than the proxy (never set it on a server members reach directly).
5. Keeping it running
- From a shell, it runs in the background.
tephrostarts the server as a background process (its output goes totephro.login the data directory) and comes back once it is serving, with the process id and the log path; if it dies at once — the port is taken, or not yet granted — you see the log's end right there. Closing the terminal changes nothing. Stopping it:tephro admin shutdownfrom any shell, orQin the status view; both are the same clean stop as Ctrl-C.tephro --foregroundkeeps it in the terminal instead (Ctrl-C stops it);tephro --tuiserves and shows the status view in the same terminal. - The view is separate from the process. However it runs — in the background, under systemd, in a container, from a Windows shortcut —
tephro tuifrom any terminal attaches the live status view to it, andqthere only detaches. No tmux needed. Under systemd the socket belongs to thetephrouser, sosudo -u tephro tephro tui --data-dir /var/lib/tephro. On Windows, runtephro.exefrom a shortcut, thentephro tuiin any PowerShell window. - Under systemd or in a container nothing changes: with no terminal the server stays in the foreground, which is what the service manager or the runtime expects. (It also recognises systemd and the common container runtimes when they do hand it a terminal.)
- systemd:
server/deploy/tephro.service— hardened, restarts on failure, data in/var/lib/tephro. - Container:
podman run -d --name tephro -p 443:443 -p 443:443/udp -v tephro-data:/data registry.gitlab.com/auhunt/tephro/server(amd64 and arm64). The image is the release binary and nothing else; the whole instance lives in the/datavolume. Admin commands run inside it:podman exec tephro /tephro admin invite --owner. Upgrade withpodman pulland a restart. To build it yourself:make release-server, thenpodman build -f server/deploy/Containerfile .. - Backup: stop the server, copy the data directory. That is the entire instance — database, config, uploads, certificate.
- Upgrade:
sudo tephro admin upgradefetches the newest release for your OS and architecture, verifies the maintainer's signature and the checksum before anything touches disk, then asks before it swaps the binary and restarts the server — every call drops for a few seconds. Under systemd the service keeps its PID and its journal; run it with sudo because/usr/local/binand the unit'sProtectSystem=strictkeep the service itself from writing there. A background server started withtephrokeeps itstephro.log. In a container, pull the new image and recreate the container instead; the command says so. If you set the port capability withsetcap, the upgrade re-applies it when run as root, or prints the line.--checkshows what would happen;tephro admin restartrestarts without upgrading; the status view'sUkey is the same flow: it asks y/n before downloading anything, then confirms again before the swap. By hand it is still just: replace the binary and restart (re-runsetcapon Linux if you used it). An instance from before 2026-09-18 has no AFK channel — the bootstrap only creates channels on an empty instance — so add one once:tephro admin channel add AFK --afk, or tick AFK channel on a voice channel in the client's Admin → Channels. The server tells you in the log and the status view when a newer release exists; it never updates itself, because that would drop every live call — you run the upgrade.
6. Day to day
| Task | Where | |||
|---|---|---|---|---|
| Invite people, approve join requests, roles, channels, moderation, audit log | The desktop client, as an administrator | |||
| Mint an invite from the box, list or revoke invites, add or remove a channel, transfer ownership, issue a recovery code, change the instance name or admission mode, see status, look up your public IP, stop the server | tephro admin … (run tephro admin help). tephro admin invite --note "for Sam" mints a personal key (--public a link, --local also prints the loopback link for a client on this box); tephro admin invites lists the keys that can still admit someone (--all for spent and revoked ones); tephro admin revoke <key> ends one; tephro admin shutdown stops the server | |||
| The shared tag list | tephro admin tags lists it; tags add NAME, tags rename OLD NEW (--merge folds two entries into one), tags delete NAME; the status view's 6 screen does the same. max_tags (200) caps it | |||
| Park idle voice members | afk_timeout_minutes (10) is how long a silent member sits in voice before the server moves them to the AFK channel and shows them away; 0 turns it off. Needs an AFK channel (tephro admin channel add AFK --afk) | |||
| Help | tephro -h lists the commands; tephro admin -h the subcommands; tephro admin <cmd> -h one subcommand's flags | |||
| Flag members on VPN or hosting addresses | tephro admin vpn-list update fetches the range list (one request, only when you run it; vpn_list_url in the config), then tephro admin vpn-list and the members screen mark them vpn. A flag for you to weigh, never a block. ip_ban_hours makes a ban also refuse the person's last addresses for a while; it is 0 unless you set it | |||
| Disconnect, time out, ban or unban someone without a client | `tephro admin disconnect | timeout | ban | unban <username> [--reason S] (timeout takes --for 2h or --until <RFC 3339>; without either it clears the timeout), then kill -HUP <pid> (or systemctl reload tephro) so the running server ends their connection; or select them on the status view's members screen (2) and press x, t, b or u` — that one applies at once. Disconnect lets them straight back in; timeout refuses their login until the time; ban keeps them out |
| Live view of connections, members, voice, storage, recent log; invites | tephro tui against the running server, or tephro --tui to serve and watch in one terminal; 2 is the members screen (disconnect, timeout, ban, unban there), 4 the invites screen — n mints a key through a short form (who it is for, personal or public, approval, lifetime), r revokes, y copies the selected link — s the settings screen, q detaches (the server keeps running), Q stops the server after a confirm. Do not share your screen while an invite is on it | |||
| Change a setting | Admin → Server in the desktop client (every key, applied at once where that is safe, the rest marked restart), or press s in the status view (every setting, one line of explanation each, saved to tephro.toml and applied at once where that is safe — the notice says which keys need a restart), or tephro admin config set <key> <value> from a shell followed by kill -HUP <pid> (or systemctl reload tephro), or edit tephro.toml (every key is commented), or the matching --flag / TEPHRO_* variable and restart. tephro admin config lists them all with their current values |
Attachment limits (max_upload_mb, user_quota_mb, instance_quota_mb), custom-emoji limits (max_emoji_kb, max_emoji), the pin limit (max_pins), video caps (video_cap_mbit, video_cap_channel_mbit) and the per-member send ceilings (max_camera_kbit, max_screen_kbit — what one camera or screen share may send, which clients honor) are yours to set; the defaults are generous and the server refuses honestly when a cap is hit rather than degrading everyone. disabled_codecs = "av1" takes a codec away from your members (vp8, vp9, av1); by default all three are allowed and each sender picks in the share dialog, so a member's stream is limited by nothing you did not turn on. Link previews are on by default: the server fetches a title and picture for links members post, so the link's host sees your server's address and never a member's. link_preview_hosts = "youtube.com, *.wikipedia.org" limits it to sites you name, and link_previews = false turns it off if you would rather it made no outbound requests at all. The pictures and videos it keeps for previews are limited to 10 MiB and 50 MiB each (max_preview_image_kb, max_preview_video_mb); anything larger is left out of the preview. Lower them on a small disk, because previews do not count against the attachment quotas, or set one to 0 for no limit. Direct messages between members are on by default and end-to-end encrypted, so your server holds only ciphertext; dms = false turns the relay off.
Something is wrong
permission deniedon startup: section 3.- Members cannot connect on a bare IP: check the fingerprint they see matches
tephro admin fingerprint; send them a full invite link so it is verified for them. - Voice connects but nobody hears anything: UDP 443 is not reaching the server. Behind a router, forward UDP as well as TCP, the same port on both sides, and set
public_ip(tephro admin public-ipprints it; the server never looks it up on its own). On a VPS, open UDP 443 in the provider's firewall. Members on networks that block UDP entirely fall back to ICE-TCP on 443 automatically. - Logs: stdout when service-managed (JSON),
tephro.login the data directory when the terminal shows the status view or the server runs in the background.Lin the status view opens that file in your pager. While anyone is in voice the log carries amedia …line per stream every five seconds: attach it to a quality report.