Run with Docker
SharpMUSH runs as a small set of containers: the game server (which also serves the web portal), the connection server (telnet and the browser WebSocket), NATS for messaging, and a database. Docker Compose brings the whole thing up together.
The stack is deliberately modest. It runs comfortably on a €4–6/month VPS, or free on Oracle Cloud’s Always Free ARM tier. Kubernetes is not needed at this scale.
Deploy a game
Section titled “Deploy a game”The maintained deployment lives in deploy/
in the SharpMUSH repository. It uses SurrealDB embedded — a RocksDB directory on disk, with no
separate database server to run or back up separately.
Pick your entry point based on how you terminate TLS:
| File | Use it when |
|---|---|
docker-compose.prod.yml | The box faces the internet directly. Caddy gets you automatic HTTPS. |
docker-compose.cloudflare.yml | You would rather open no inbound web ports. A Cloudflare Tunnel dials out instead, hiding the origin IP. |
-
Get the files onto the host.
Terminal window git clone https://github.com/SharpMUSH/SharpMUSH.gitcd SharpMUSH/deploycp .env.example .env -
Edit
.envand set your domain.Terminal window SHARPMUSH_DOMAIN=mush.example.comBackups are opt-in and off by default — leave that section commented out for now. If you chose the Cloudflare variant, also set
CLOUDFLARE_TUNNEL_TOKENhere. -
Bring the stack up.
Terminal window docker compose -f docker-compose.prod.yml up -d --buildThe portal comes up on
https://<your-domain>, and telnet on port 4201. -
Claim the admin account.
Visit
https://<your-domain>/setup. The first visitor after a fresh install claims the admin account and creates the God character. Do this immediately after deploying — the setup route is open until someone completes it.
Point the browser terminal at the right place
Section titled “Point the browser terminal at the right place”This is the one piece of configuration that catches people out.
The portal’s in-browser terminal talks to a WebSocket endpoint at /ws, which is served by the
connection server (:4202) — not by the main server. The supplied Caddyfile already routes
https://<your-domain>/ws to the connection server, so in the portal set the terminal’s
Server URI to:
wss://<your-domain>/wsSame origin, so no mixed-content or CORS problems. The default ws://localhost:4202/ws is for local
development only — over HTTPS, browsers refuse a plaintext ws:// connection.
If you are not using the supplied Caddy config (say you terminate TLS at a load balancer), you
must publish 4202 yourself or add an equivalent /ws proxy route, in addition to publishing the
server’s HTTP port.
| Port | Service | Exposed to the internet? |
|---|---|---|
| 80 / 443 | Caddy — portal, SignalR, and /ws → connection server | yes |
| 4201 | Telnet | yes |
| 8080 | ASP.NET server (HTTP) | no — internal, behind the proxy |
| 4202 | Connection server HTTP / /ws WebSocket | no — internal, reached via the /ws route |
| 4222 / 8222 | NATS client / monitoring | no — internal only |
Keeping it running
Section titled “Keeping it running”Updates are automatic. Every merge to main runs the full test suite and publishes
sharpmush/sharpmush-server:dev and sharpmush/sharpmush-connectionserver:dev to Docker Hub. A
watchtower service polls every five minutes and recreates those two services when the tag moves,
pruning superseded images. Nothing on the box ever builds, and no inbound access is required.
There is no separate client image — the Blazor portal is baked into the server image at build time.
To update everything else (NATS, cloudflared, the backup sidecar), or to force an app update
rather than waiting for the poll:
git pulldocker compose pulldocker compose up -dThe app-data volume persists across updates, so the world is untouched. Migrations are recorded in
the database and re-applied only when new, so unattended restarts are safe.
Starting on boot
Section titled “Starting on boot”restart: unless-stopped brings containers back after a reboot, but it replays the last state
rather than the declared one — a docker compose down or an edited compose file survives the
reboot, and the box quietly drifts from what deploy/ says it should be.
The supplied sharpmush.service unit closes that gap by running docker compose up -d on boot:
# as root, on the hostln -s /opt/SharpMUSH/deploy/sharpmush.service /etc/systemd/system/sharpmush.servicesystemctl daemon-reloadsystemctl enable --now sharpmush.serviceEdit WorkingDirectory and the compose filename in the unit if your checkout is not at
/opt/SharpMUSH.
Backups
Section titled “Backups”Backups are opt-in and off by default: the backup service sits behind a Compose profile, so an
unconfigured box gets no backup container rather than one crash-looping against a repository that
does not exist.
Fill in the restic settings in .env, uncomment COMPOSE_PROFILES=backup, and initialise the
repository once:
docker compose run --rm backup restic initFrom then on it snapshots the app-data volume — the SurrealDB store and wiki assets, i.e. the
entire game — to your bucket nightly, keeping 7 daily and 4 weekly snapshots. The volume is mounted
read-only, so a backup run can never corrupt live data.
Full details, including restore procedure and the Cloudflare Tunnel walkthrough, are in
deploy/README.md.
Running the development stack
Section titled “Running the development stack”The Compose files at the repository root are a different thing: they build from source and are aimed at hardcode development rather than at hosting a game. They use ArangoDB and expose the metrics endpoints, and they expect the .NET toolchain described in Install for Development.