Deploy · self-hosted on your own machine or VPS
Run it yourself.
The repository's deploy/ folder has everything to run a server on one Linux machine (written for Ubuntu 24.04): HTTPS and WSS through Caddy, migrations on every deploy, daily backups with a tested restore, and a hardened SSH login. This page is a summary; the deployment guide walks through every step.
Choose the install path once
| Docker Compose | systemd | |
|---|---|---|
| The server runs | in a container (non-root, read-only, health-checked) | as a service of a dedicated user, with systemd's sandboxing |
| Database | MySQL 8.4 or PostgreSQL 16 in a container | MySQL, PostgreSQL or SQLite on the machine |
| Caddy | in a container | Caddy's own package |
| Migrations | the migrate service runs before the server starts | ExecStartPre=… migrate before every start |
| Backups | net-backend-backup (systemd timer) dumps inside the database container | the same timer, dumping directly |
| Updates | git pull + docker compose up -d --build | build, then install.sh again (or copy the binary + restart) |
Both paths install the reference server: the framework with the Auth, Storage and Chat modules, configured from config.toml. Your own game server has the same command line (serve, migrate, config check, user:create, …), so every file works for it unchanged.
Before you start
- A machine with Ubuntu 24.04, a public address and SSH access by key (the SSH hardening guide first). 2 vCPU and 4 GB of memory are a comfortable start; see the measurements.
- A host name for the API (for example
api.example.com) whose DNS record points at the machine. - Open ports 80 and 443 (TCP) and 443 (UDP, HTTP/3). Nothing else: the server and the database listen on loopback or on private container networks.
Both install paths were installed from scratch and tested on a real Ubuntu 24.04 machine.
HTTPS and WSS with Caddy
- HTTPS and WSS with automatic certificates; HTTP redirects to HTTPS; HTTP/3 on UDP 443. One
reverse_proxyserves the API and the WebSocket hub, and open WebSockets survive a configuration reload. - A 5 MB request limit for the whole site: enough for the storage module's largest request, a batch save of up to 4 MiB of values.
- Logs that never contain tokens: the
AuthorizationandCookieheaders are deleted from log lines, and atokenquery parameter is removed from the logged URL. - A trusted proxy: the server believes
X-Forwarded-Foronly from Caddy, so per-address rate limits and WebSocket caps count real clients. The server itself terminates no TLS.
Migrations on every deploy
Migrations run automatically before the server starts: a one-shot migrate service in Docker Compose, ExecStartPre=… migrate under systemd.
Backups and restore
A daily systemd timer backs up the database and keeps each backup 14 days:
| Database | How |
|---|---|
| MySQL / MariaDB | mysqldump --single-transaction (consistent, writers keep going), checked for its final "Dump completed" line |
| PostgreSQL | pg_dump -Fc, checked with pg_restore --list |
| SQLite | the online backup API (consistent while the server writes), PRAGMA integrity_check, gzip |
A restore that is safe to run. net-backend-restore first checks the whole backup, before anything is stopped or dropped. Then it takes a safety backup of the current state, stops the server, loads the backup (PostgreSQL in one transaction, so a failed load leaves the old data), runs migrate, starts everything and waits until /readyz answers. If a step after the stop fails, it prints the command that puts the safety backup back. The database password never appears on a command line.
sudo net-backend-restore /var/backups/net-backend/net-backend-mysql-20261002T033012Z.sql.gz --yes
A backup on the same disk does not survive the loss of the machine: copy /var/backups/net-backend/ elsewhere every day (for example with restic or rclone).
Measured on a small VPS
Measured on a 2 vCPU / 7.8 GiB virtual machine (Ubuntu 24.04, MySQL 8 or PostgreSQL 16 on the same machine, Caddy in front). The HTTPS rows ran load_test on that same machine, so it shared the 2 vCPU with the server, Caddy and the database:
| What | Measured |
|---|---|
| 10 000 authenticated WebSockets over HTTPS through Caddy | all connected in 56 s (TLS handshakes), none closed during the hold; server ~15 KiB per socket, Caddy ~121 KiB |
| A redeploy: 5000 sockets closed with 1001 by a restart | all 5000 reconnected (p99 17 s, bound by TLS handshakes on the shared CPUs) |
| Chat over HTTPS: 200 members, 50 senders at 1 message/s, every message stored | 600 000 of 600 000 deliveries, p99 294 ms (MySQL), 241 ms (PostgreSQL) |
| Room fan-out on loopback (one sender, 64-byte pushes) | 105 000 deliveries/s into 200 members; 149 000/s into 1000 members |
| 1000 players saving for the first time at the same moment | all 1000 stored in 8.6 s (MySQL), 7.4 s (PostgreSQL) |
| 4 MiB batch saves through Caddy | 10 of 10, median 0.97 s (MySQL), 0.47 s (PostgreSQL) |
| HTTP through Caddy, 64 connections | /v1/info ~4 500 requests/s; GET /v1/account 770 (MySQL) / 890 (PostgreSQL); a storage read 940 / 1 050 |
Memory: ~15 KiB per socket in the server plus ~120 KiB in Caddy, so roughly 1.4 GB per 10 000 connected players, on top of the database's buffer pool and the system (the default ws.max_connections = 10000 wants a 4 GB machine). Raise ws.max_connections only after a load test on the machine itself.
Load-test your own machine with load_test, a tool in the repository: it creates accounts, opens sockets, runs chat fan-out, DM bursts, simultaneous first saves, 4 MiB batch puts and HTTP request rates through Caddy, and prints latency percentiles. The full capacity section →
Security checklist
- SSH by key only, no root login, a firewall that opens 22 / 80 / 443 only, fail2ban (SSH hardening guide).
- Secrets live in files with tight permissions, never in
config.tomland never in the repository. - Tokens only in headers (
ws.query_token = false, the default). - Metrics on loopback only;
openapi.enabled = falseif the API description should not be public. - The admin role on as few accounts as possible; every admin action is in the audit log.
- Backups leave the machine, encrypted, and a restore was practised.