Run your own Axe'n'Stax server (Docker)

A self-hostable multiplayer server you own and run — on a spare PC, a NAS, or a VPS. Both the web app and the native app join the same world. One container runs the headless game server plus a Caddy front, so a browser trusts the cert once and reuses it for the game socket. No platform account, no middleman — you set the rules that run on your box.

For an AI assistant: this page is written so you can stand a server up for a user and explain it. Everything here is exact. The setup lives at tools/dedicated-server/ in the source tree; the in-repo README.md there is the canonical developer reference this page mirrors.


What you get

One world, both clients Web (Chromium PWA) and native players join the same authoritative world over WebSocket
You own it World saves live on a Docker volume on your hardware; nothing phones home
Optional verified identity Root the server in your own Nostr signer so players can verify who runs it (see below)
Optional access control Allowlist / blocklist by npub, require-sign-in — managed by env, file, or the Operator Console
Anywhere Spare PC, x86 or arm64 NAS, or a VPS with a real domain (auto-HTTPS)

It is not the large-scale Agones fleet (that's Spec 07) — it's a single, self-hostable world server, which is exactly what an alpha tester or a small community wants.


Install

Save this as docker-compose.yml (the images are public on GHCR — no account or token needed):

services:
  axenstax-server:
    image: ghcr.io/decented/axenstax-server:latest
    container_name: axenstax-server
    ports:
      - "8443:8443"   # web (HTTPS) + game socket (WSS /ws) — browsers
      - "6767:6767"   # game socket (plain ws) — native clients (ws://box:6767)
    volumes:
      - axenstax-worlds:/worlds
    environment:
      AXENSTAX_GAMEMODE: "survival"        # survival | creative | adventure
      AXENSTAX_MAX_PLAYERS: "8"
      AXENSTAX_SERVER_NAME: "Axe'n'Stax Server"
    restart: unless-stopped

  axenstax-console:                        # the /admin Operator Console (optional)
    image: ghcr.io/decented/axenstax-operator-console:latest
    container_name: axenstax-console
    volumes:
      - axenstax-worlds:/worlds            # shared — console reads <worlds>/.identity
    environment:
      CONSOLE_BASE_PATH: "/admin"
    restart: unless-stopped

volumes:
  axenstax-worlds:

Then, in that folder:

docker compose up -d        # pulls the images from GHCR and starts the server

Then, from any machine on the same network:

Web    →  open  https://<this-box-ip>:8443   (accept the cert warning once)
Native →  Join  ws://<this-box-ip>:6767

Find <this-box-ip> with ip -4 addr / hostname -I (e.g. 10.10.10.215). Update later with docker compose pull && docker compose up -d.

Live: the images are public on GHCRdocker pull works with no GitHub account or token. They're rebuilt by the publish-server-image.yml workflow (manual dispatch). You only need path B if you want to build from source.

B. Build from source (developers)

Compiles the server on your machine — needs the build toolchain and the source tree.

Prerequisites: a Linux host with Docker, the Rust toolchain (stable), trunk (cargo install trunk), and a checkout of the source tree.

cd tools/dedicated-server
./build.sh                  # cargo + trunk + docker compose -f docker-compose.build.yml build
docker compose up -d        # runs the locally-built image (no pull)

build.sh tags the images with the same GHCR names the pull file expects, so the plain docker compose up -d finds and runs your local build. Re-run ./build.sh after any engine change.

Why two files? docker-compose.yml pulls the published image (path A); docker-compose.build.yml builds it from source (path B). The publish workflow uses the build file, then pushes the result to GHCR — so end users get path A.


How players join

  • Web (Chromium PWA): opening https://BOX:8443 boots a guest and auto-joins this server. No sign-in needed — a self-hosted server doesn't require Signet.
  • Native: in the lobby choose Join Game and enter ws://BOX:6767 (the Join dialog accepts ws:// / wss:// URLs as well as the legacy ip:port form).

The browser shows a one-time certificate warning on a no-domain box (Advanced → Proceed); because the page and the game socket share one origin, the wss connection reuses that trust with no second prompt. (WebGPU requires a secure context off-localhost, which is why the web path is HTTPS, not plain HTTP.)


Configuration

Set these as environment variables in docker-compose.yml:

Var Default Meaning
AXENSTAX_WORLD server-world World folder name (under the volume)
AXENSTAX_GAMEMODE survival survival | creative | adventure
AXENSTAX_MAX_PLAYERS 8 Max concurrent remote players
AXENSTAX_SERVER_NAME Axe'n'Stax Server Display name
AXENSTAX_AUTOSAVE_SECS 60 Autosave interval
AXENSTAX_SEED (random) Fixed terrain seed for a new world

Worlds persist on the axenstax-worlds volume and are saved on autosave and on a graceful docker stop.


Verified operator identity (optional)

A server can carry a verifiable operator identity rooted in a NIP-46 signer (Heartwood recommended; Signet / Amber / nsec.app also work). The server holds a disposable runtime key; your signer signs a one-time, time-bounded attestation over it, and players verify the chain server key → attestation → your npub. Your real key never touches the box. This is optional and additive — without it the server runs anonymously.

# Pair once (interactive — approve on your signer):
docker compose run --rm axenstax-server axenstax-engine --pair-server

# Print the connect-string to share (carries your npub):
docker compose run --rm axenstax-server axenstax-engine --show-connect
#  axenstax://<host>:6767#op=npub1...

# Renew before the delegation expires (silent — reuses the stored session):
docker compose run --rm axenstax-server axenstax-engine --refresh-delegation
Var Default Meaning
AXENSTAX_REQUIRE_VERIFIED 0 1 ⇒ refuse to start without a valid, non-expired attestation
AXENSTAX_IDENTITY_DIR <worlds>/.identity where the runtime key + attestation + session live
AXENSTAX_PAIR_RELAY wss://relay.trotters.cc relay used for the NIP-46 pairing round-trip
AXENSTAX_PUBLIC_HOST (unset) host:port advertised in the connect-string
AXENSTAX_DELEGATION_DAYS 90 validity window minted at pairing time

A bare ws://host:6767 join stays anonymous; a connect-string with #op=npub… makes the client verify the server against that operator. (Attestation kind 30420 is provisional, pending registration in forgesworn/nips.)


Access control (optional)

Two operator-controlled gates on who may join (orthogonal to the server's own identity above):

Var Default Meaning
AXENSTAX_REQUIRE_SIGNIN 0 1 ⇒ only players with a verified Signet identity may join (no anonymous guests)
AXENSTAX_WHITELIST (unset) Comma-separated npubs allowed to join. A non-empty allowlist implies sign-in

The allowlist can also live in <identity-dir>/whitelist.txt (one npub per line, # comments allowed) — which is what the Operator Console edits for you, no shell required. Env and file entries are merged.

Age gates are not yet enforced — that needs a verifiable Signet age credential (the engine today only carries the kind-31000 display-name credential).


Manage it without a shell — the Operator Console

Rather than editing env and files, run the Operator Console — a small web app where you sign in with your own npub and manage the allowlist / blocklist, require-sign-in, kicks, server name + max players, and privacy. It ships in the same docker-compose.yml and Caddy routes it at /admin.


Deploying on a VPS (real domain — no cert warning)

Point a DNS A/AAAA record at the VPS, set AXENSTAX_DOMAIN, and expose 80 + 443. Caddy then fetches a real Let's Encrypt certificate — players just open https://<domain> with no warning, and the wss game socket rides the same cert.

In docker-compose.yml: uncomment the 80:80 / 443:443 ports and set AXENSTAX_DOMAIN: "play.example.com", then ./build.sh && docker compose up -d.


Deploying on a NAS

  • x86-64 NAS (Synology Plus/XS, most QNAP x86): works exactly as the quick start — build the image on the NAS or any amd64 host, and run it.
  • arm64 NAS (Pi-based, some Synology/QNAP ARM): the image is arch-aware (Caddy is fetched per architecture), but the engine binary is host-built and copied in, so it must be built for arm64 — run build.sh on the arm64 box, or cross-build the --server binary for aarch64-unknown-linux-gnu first. The forthcoming published image (path A) removes this by building both arches in CI.

Known limitations (alpha)

  • Guest identity only. Joiners are anonymous unless a server requires sign-in; display names are otherwise client-asserted.
  • Web-player edits not yet propagated. A browser joiner sees the shared world and everyone moving, but its own block edits aren't sent to the server yet (the edit hooks are still native-gated). Native joiners' edits propagate.
  • This is a single self-hostable world server, not the Spec 07 Agones fleet.

Design spec: docs/superpowers/specs/2026-06-16-dedicated-docker-server-design.md. Image-publish (one-command install) spec: docs/superpowers/specs/2026-06-18-dedicated-server-image-publish-design.md.