Files
voice-cat/docs/deployment.md
T
Talon 57a63cc1cd
.NET port / test (macos-latest) (push) Canceled after 0s
.NET port / test (ubuntu-24.04) (push) Canceled after 0s
.NET port / test (windows-latest) (push) Canceled after 0s
.NET port / apple-client (push) Canceled after 0s
Publish Windows and Linux servers together
2026-09-20 01:15:52 +02:00

10 KiB

Deployment & Self-Hosting

Managed server deployment checkpoint

The .NET server preserves protocol v2 and the native schema/credentials. Publish a self-contained executable (no installed .NET runtime required):

# Publishes both win-x64 and linux-x64 by default.
./dotnet/publish-server.ps1
# Publish just one target when needed.
./dotnet/publish-server.ps1 -Runtime linux-x64
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe --help
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe account add Operator --admin --data-dir ./voicecat-data
./dotnet/artifacts/server/win-x64/VoiceCat.Server.exe --data-dir ./voicecat-data --allow-guests false

Account add/reset use a hidden password prompt, redirected standard input, or VOICECAT_ADMIN_PASSWORD. Passwords are never accepted as command arguments or logged. Account delete/list work against the same database, including while the server runs. Provisioning grants administrator access only through the local command's --admin; in-band account creation remains non-admin. Restrict access to the data directory.

Defaults are 0.0.0.0:8384 TCP+UDP, guest access enabled, 64 connections, 15-second TLS handshakes, 45-second idle expiry and 15-second sweeps. Override with flags or environment:

Flag Environment variable
--data-dir VOICECAT_DATA_DIR
--bind VOICECAT_BIND_ADDRESS
--port VOICECAT_BIND_PORT
--name VOICECAT_SERVER_NAME
--allow-guests VOICECAT_ALLOW_GUESTS
--max-connections VOICECAT_MAX_CONNECTIONS
--handshake-seconds VOICECAT_HANDSHAKE_TIMEOUT_SECONDS
--idle-seconds VOICECAT_IDLE_TIMEOUT_SECONDS
--reaper-seconds VOICECAT_REAPER_INTERVAL_SECONDS
--auth-burst VOICECAT_AUTH_BURST
--auth-refill-seconds VOICECAT_AUTH_REFILL_SECONDS

Command arguments override environment values. --print-config validates and prints JSON without creating files; --print-fingerprint prints the persisted leaf-certificate SHA-256 pin. Startup emits one JSON ready event with both fingerprints and actual TCP/UDP ports. Bind accepts IP literals; IPv6 listeners are IPv6-only. Open/forward both protocols. An exclusive data-directory instance lock prevents duplicate managed server processes. Ctrl+C and Unix SIGINT/SIGTERM stop all transport tasks; shutdown has a ten-second deadline. Fatal listener/media/reaper failure exits the host rather than leaving a broken listener. --health-check HOST:PORT performs a real protocol TLS 1.3 handshake and emits JSON. Automation can add --expect-fingerprint SHA256 to verify the persisted certificate.

Password authentication is limited before Argon2 by source address and username across connections: burst 5, refill one attempt per ten seconds. Starting at three failed attempts, backoff grows from one to thirty seconds. Success clears backoff but does not restore tokens. State is bounded to 4096 keys; idle entries retire after ten minutes when full. Throttle and credential failures share the generic auth error. Limits are process-local.

The publish script produces Windows x64 and Linux x64 self-contained outputs in one command by default. It uses separate per-RID lock files so deployment and development restore graphs remain reproducible. Pass -Runtime to publish only one target. The Linux smoke starts the published ELF, validates TLS health and the pin, checks mode 0700 data creation, sends SIGTERM and requires a clean exit:

sh deploy/linux/smoke.sh dotnet/artifacts/server/linux-x64/VoiceCat.Server

deploy/linux/voicecat.service runs under a systemd dynamic user with a private state directory, no capabilities and filesystem/kernel hardening. Install a published binary and the unit from deploy/linux/ as root, or use the helper there. The default Dockerfile now builds the managed server with locked packages and runs it as UID 1654 in Microsoft's chiseled .NET runtime-dependencies image. Compose drops all capabilities, sets the root filesystem read-only and persists only /data.

For operational endurance against an already running server:

./dotnet/soak-server.ps1 -HostName 127.0.0.1 -Port 8384 -Minutes 30 -Pairs 4

Every cycle creates independent managed processes that exchange text and decoded voice. A short 16-session published-server soak is part of this checkpoint. A release candidate still needs the long soak on its target host. TOML/reload and signed image publication remain before broad production rollout.

The product goal: someone looks at this and thinks "oh, I (or my agent) can stand this up in a few minutes." Everything below is in service of that. Three install paths, all zero-config and encrypted by default.

1. The three paths

docker build -t voicecat:local .
docker run -d --name voicecat \
  -p 8384:8384/tcp \      # control (TLS 1.3)
  -p 8384:8384/udp \      # media (encrypted)
  -v voicecat-data:/data \
  --read-only --tmpfs /tmp:rw,noexec,nosuid,size=16m \
  --cap-drop ALL --security-opt no-new-privileges \
  voicecat:local

That's the whole thing. On first start it generates its Ed25519 identity + self-signed cert, creates the SQLite database under /data, prints the server fingerprint (for clients to verify), and listens. Control and media share one port number on TCP+UDP to keep firewall rules trivial.

A docker-compose.yml is provided for people who prefer it, but it isn't required.

B. Single static binary

# download for your OS, then:
./voicecat-server          # uses ./voicecat-data/ , prints fingerprint, runs

The server is a single statically linked executable (mbedTLS, libsodium, opus, sqlite, etc. linked in — all permissive licenses). No runtime, no shared libraries to install, no package manager. Linux (primary), macOS, and Windows builds.

C. From source

git clone … && cd voice-cat
cmake --preset server-release      # vcpkg fetches & pins all deps; auto-triplet (Linux/macOS/Windows)
cmake --build --preset server-release
./build/server-release/bin/voicecat-server

One cmake invocation; vcpkg (manifest mode) resolves the dependency graph reproducibly. The server-release preset produces an optimized, stripped binary (-s linker flag) — smaller executables suitable for distribution. Works on Linux (primary), macOS, and Windows; the vcpkg triplet is auto-resolved by cmake/voicecat-toolchain.cmake. No system packages to chase.

2. Zero-config defaults

The server runs with no config file at all. Sensible defaults:

Setting Default
Encryption On, always (not configurable off)
TLS cert / identity Auto-generated on first run, persisted to the data dir
Database Embedded SQLite in the data dir (no external DB)
Guests Enabled (so the very first connect "just works"); easily disabled
Ports 8384 TCP + UDP
A default channel One "Lobby" voice/text channel created on first run
Opus policy 48 kHz, 20 ms frames, mono/VOIP defaults; per-channel overrides allowed
Argon2id cost Auto-tuned to the host on first run

Override only what you care about, via env vars or an optional server.toml:

# server.toml — every key is optional
server_name   = "Cats United"
allow_guests  = false
bind_port     = 8384
data_dir      = "/data"

[tls]                       # only if you want a real CA cert; otherwise self-signed
cert_file = "/data/fullchain.pem"
key_file  = "/data/privkey.pem"

[opus.defaults]             # default Opus policy for new channels
mode = "mono"
bitrate_bps = 24000
frame_ms = 20
fec = true
dtx = true

[opus.limits]               # server-enforced ceilings (bound bandwidth)
max_bitrate_bps = 128000    # channels can't be configured above this

Every key also has an VOICECAT_* env var form, which is what the Docker path uses.

3. Connecting (client side)

  • Pure direct-connect. Enter host:port (and a nickname or account). There is no central directory or server browser — you connect to a server you know.
  • Saved server list. The client keeps a local list of saved servers (host:port, pinned fingerprint, nickname/credentials per server) so you can store several and pick one to join. This lives entirely in the client.
  • On first connect the client shows the server's fingerprint and pins it (TOFU). No accounts or certs needed to try it; if the operator disabled guests, the client prompts for the username/password an admin gave you.

That's the entire flow: run the server, share host:port + fingerprint, friends save it and connect.

3a. Provisioning accounts (admin)

Accounts are admin-provisioned — there is no self-serve registration. Two equivalent ways, both writing the same SQLite store:

# CLI against the server's data dir or a running server
voicecat-admin account add   <username>          # prompts for / generates a password
voicecat-admin account reset <username>
voicecat-admin account del   <username>
voicecat-admin account list

…or from the in-app admin interface (a user with the admin permission), which sends the privileged CreateAccount/ResetPassword/DeleteAccount control messages over TLS (protocol.md §3). Guests need no provisioning; they just pick a nickname (if guests are enabled).

4. Why it stays this easy (design constraints that protect the goal)

  • No external services. No separate database, no Redis, no TURN/STUN server, no reverse proxy required. SQLite is embedded; media is plain UDP.
  • No certificate chore. Self-signed + Ed25519 TOFU means encryption needs zero operator action. A domain owner can drop in a Let's Encrypt cert, but never has to.
  • One port pair. TCP+UDP on the same number; one firewall/port-forward rule.
  • Static linking + permissive licenses. The binary has no install-time dependencies and can be redistributed (including closed-source) without copyleft obligations.
  • Agent-friendly. The run command is a single line with no interactive prompts, the server logs its fingerprint and listen address in machine-readable form, and --help / --print-config expose everything an automation needs. Health endpoint for liveness checks.

5. Operational niceties (planned, not blocking v1)

  • Signed, multi-architecture image publication.
  • Graceful reload of server.toml on SIGHUP.
  • voicecat-admin (see §3a) also handles bans and channel admin, talking to the same SQLite file or a running server.
  • Prebuilt images for linux/amd64 + linux/arm64 (Raspberry Pi / cheap VPS friendly).