Blocks Beyond the Stars is designed so players can host their own server on a Windows PC, a Linux box, or a VPS — without installing .NET (the packages are self-contained).
Download or build a package for your platform:
| Platform | Package |
|---|---|
| Windows x64 | blocks-beyond-the-stars-server-win-x64.zip |
| Linux x64 | blocks-beyond-the-stars-server-linux-x64.zip |
| Linux ARM64 | blocks-beyond-the-stars-server-linux-arm64.zip |
Build them yourself from a checkout with the .NET 10 SDK:
./scripts/publish-server.ps1 # Windows./scripts/publish-server.sh # Linux/macOS1. Unzip the package.
2. Run the server executable:
Windows: BlocksBeyondTheStars.GameServer.exe
Linux: ./BlocksBeyondTheStars.GameServer
3. A default config/server.json is created on first launch.
4. (Optional) Run the admin UI: BlocksBeyondTheStars.Api(.exe)
5. Friends connect to your IP on the gameplay port.
On low-power ARM64 boards, prefer an SSD over a microSD/eMMC for the world database to reduce wear and improve autosave performance.
Created on first run; editable directly or through the admin UI.
| Key | Meaning | Default |
|---|---|---|
serverName |
Display name | Blocks Beyond the Stars Server |
worldName |
Save folder under saves/ |
world_001 |
gameplayPort |
UDP gameplay port for native clients; also the HTTP/WebSocket port when WebSocket is enabled | 31415 |
adminPort |
HTTP port for the admin UI | 31416 |
maxPlayers |
Connection cap | 12 |
serverPassword |
Required to join (empty = none) | "" |
whitelistEnabled / whitelist |
Restrict who may join | false / [] |
adminPlayers |
Player names granted the Admin role on join (CLI: --admins "a,b") |
[] |
| — | Allow admin cheat commands (/tp, /give, …) in every mode (CLI only: --admin-cheats true; the bundled singleplayer/host launcher always passes it) |
false |
adminPassword |
Required for admin API calls | "" |
autoSaveIntervalMinutes |
Autosave cadence | 5 |
backupIntervalMinutes |
Backup cadence | 60 |
viewDistanceChunks |
Chunk stream radius | 4 |
tickRate |
Simulation Hz (10–20 recommended) | 15 |
seed |
World seed (0 = derive from world name) | 0 |
startPlanet |
Starting planet type | varied |
adminBindAddress |
Admin UI bind address | 127.0.0.1 |
enableWebSocket |
Enable browser/WebGL WebSocket gameplay transport | false |
webSocketBindAddress |
WebSocket HTTP bind host (+ for all interfaces/reverse proxies) |
localhost |
databaseProvider |
Save backend: sqlite or postgresql |
sqlite |
postgresConnectionString |
PostgreSQL connection string (prefer env/secret) | "" |
aiLevel |
Optional AI text backend: Off, Suggest (AI missions land as drafts), Auto (published) — see §8 |
Off |
aiBackendUrl |
Base URL of the optional AI backend | http://127.0.0.1:8077 |
Every key above can also be set with a BBS_* environment variable, which is the natural way to
configure the Docker image. Precedence is server.json < environment <
command line, so env vars override the file but the in-game host's CLI flags still win.
| Variable | Maps to | Variable | Maps to |
|---|---|---|---|
BBS_SERVER_NAME |
serverName |
BBS_ADMIN_PASSWORD |
adminPassword |
BBS_WORLD |
worldName |
BBS_ADMIN_BIND |
adminBindAddress |
BBS_PORT (BBS_GAMEPLAY_PORT) |
gameplayPort |
BBS_ENABLE_WEBSOCKET |
enableWebSocket |
BBS_ADMIN_PORT |
adminPort |
BBS_WEBSOCKET_BIND |
webSocketBindAddress |
BBS_MAX_PLAYERS |
maxPlayers |
BBS_SAVES |
savesRoot |
BBS_PASSWORD (BBS_SERVER_PASSWORD) |
serverPassword |
BBS_DATA |
dataDir |
BBS_ADMINS |
adminPlayers (comma-separated) |
BBS_USERCONTENT |
userContentDir |
BBS_FLEET_ADMINS |
fleetAdminPlayers (comma-separated) |
||
BBS_SEED |
seed |
BBS_TICK_RATE |
tickRate |
BBS_START_PLANET |
startPlanet |
BBS_VIEW_DISTANCE |
viewDistanceChunks |
BBS_FREE_FLIGHT |
rules.freeSpaceFlight |
BBS_SPACE_COMBAT |
rules.spaceCombat |
BBS_SHIP_WEAPONS |
rules.shipWeapons |
BBS_SPACE_NPCS |
rules.spaceNpcEnemies |
BBS_DATABASE_PROVIDER (BBS_DATABASE) |
databaseProvider |
BBS_POSTGRES_CONNECTION_STRING (DATABASE_URL) |
postgresConnectionString |
BBS_AI_LEVEL |
aiLevel |
BBS_AI_BACKEND_URL |
aiBackendUrl |
BBS_ADMINS grants the in-game Admin role: cheats, announcements, restarts. The first player to join a
world becomes its WorldAdmin and keeps those powers for their own world.
BBS_FLEET_ADMINS is a step above and means "operator of this installation". It is the only thing that unlocks
the invisible observer mode (/spectate) and the cross-body /goto, because those reach into worlds other
people own. Two deliberate properties:
- It is never written into a save. Roles live in the player record, and player records travel with an exported/re-uploaded world — an operator-level role stored there would follow the world onto machines the operator does not control. The elevation is recomputed from config on every join.
- It is not a world option. A world owner cannot switch off the operator's ability to look at their world, which would defeat the point on a hosted fleet. The role is the whole gate; the "admin cheats" world option does not apply.
On a hosted fleet, set fleetAdmins on the WorldHost (BBS_WH_FLEET_ADMINS) — it forwards the list to every
world container as BBS_FLEET_ADMINS. Left empty (the default), observer mode is off for the entire fleet.
Reaching any world (issue #495): a fleet admin signed in with a developer account (one registered with
the secret BBS_WH_RESERVED_CLAIM_CODE) sees an extra "All worlds (operator)" section in the client's
Official Worlds screen — every world on the fleet, private ones included — and joins password-protected worlds
without the password. The gate is double-locked: developer account and fleet-admin join name. Config load
auto-adds every fleet-admin name to ReservedNames, so the name that carries this power can never be
registered or played by anyone else. Name matching is case-insensitive on both sides.
BBS_FREE_FLIGHT=true is useful for hosted WebGL realms where every player should be allowed to launch and fly
manually right away. It also upgrades older world metadata that was saved before free flight became the default,
while leaving the rest of that world's saved rules intact.
A player's name keys their server-side state (inventory, position, role). Two protections
are built in: a name that is currently online cannot join a second time, and the first
join under a name claims it with a per-install client secret — later joins must present
the same secret or are rejected ("name belongs to another player"). Only a hash of the
secret is stored in the save. The very first player ever to join a fresh world becomes its
WorldAdmin; names listed in adminPlayers get the Admin role on join.
In-game hosting: the client's main-menu Host Game runs this same server as a child process (any singleplayer save, "open to LAN" style) with
--max-players, an optional--password, and the host's name in--admins.
- Native gameplay: UDP
gameplayPort(default 31415). Forward this on your router for desktop native clients. - Browser/WebGL gameplay: when
enableWebSocket=true, the server also listens for HTTP/WebSocket upgrades ongameplayPort. Browsers connect withws://orwss://; reverse proxies and managed hosts must allow WebSocket upgrade traffic to the game server. Azure Container Apps should use HTTP/auto ingress to target port 31415 for WebGL, then clients use the app'swss://...URL. Native UDP clients still need a UDP-capable host. - Admin UI: HTTP
adminPort(default 31416), bound to127.0.0.1by default so it is not reachable from outside. Only changeadminBindAddressif you understand the risk, and always set anadminPasswordfirst.
The admin UI is its own small executable, BlocksBeyondTheStars.Api(.exe), shipped in the
server package next to the game server. It reads the same config/server.json
(resolved relative to its own folder), so run it from the server install directory. Start
it and open the dashboard in a browser:
http://127.0.0.1:31416/ i.e. http://<adminBindAddress>:<adminPort>/
The dashboard shows server/world status and lets you edit the configuration, create and
list backups, tail the server log, and manage admin missions / content packs. If an
adminPassword is configured, enter it in the dashboard's password field — it is sent as
an X-Admin-Password header with every API call. Without a password the UI relies on the
loopback bind and shows a warning in the status. Live operations on a running server
(kick/ban, start/stop) are intentionally not part of this UI.
From a source checkout the same UI runs with
dotnet run --project src/BlocksBeyondTheStars.Api. Note that it then resolves
config/server.json relative to its own build folder (bin/Debug/net10.0/), not the
game server's — for a shared config run both executables from one published install
directory.
Everything the dashboard does is plain HTTP under /api (JSON; add the
X-Admin-Password header when a password is set):
| Route | Meaning |
|---|---|
GET /api/status |
Server/world snapshot (name, world, ports, players, backups, warnings) |
GET / PUT /api/config |
Read / replace config/server.json |
GET / POST /api/backups |
List / create a world backup |
GET /api/logs?lines=200 |
Tail the server log |
GET / POST /api/missions, DELETE /api/missions/{id} |
Admin mission editor |
GET / POST /api/content-pack |
Export / import a content pack |
Example call without the dashboard:
Invoke-RestMethod http://127.0.0.1:31416/api/status -Headers @{ 'X-Admin-Password' = '<password>' }
# or: curl -H "X-Admin-Password: <password>" http://127.0.0.1:31416/api/statusThe host also serves a public-facing /portal landing page — a polished page with the
JuMaVe Games + game logos, one-click client downloads (Windows Setup.exe at /download, Linux
.AppImage at /download-linux, experimental macOS zip at /download-mac) and the in-app update URL.
See §9 for distributing the client this way.
- SQLite remains the default. A world lives in
saves/<worldName>/world.dbwithbackups/andlogs/alongside — fully portable; copy the folder to move or back up a local/self-hosted world. - PostgreSQL is opt-in for hosted dedicated servers: set
databaseProvidertopostgresql(orBBS_DATABASE_PROVIDER=postgresql) and providepostgresConnectionStringthrough an environment secret such asBBS_POSTGRES_CONNECTION_STRING. Each world is isolated into its own schema named from the world name, while logs, bug reports and JSON backup exports still usesaves/<worldName>/. - SQLite backups are transactionally consistent
.dbcopies (VACUUM INTO). PostgreSQL backups are JSON table-export snapshots (*.postgresql.json) for inspection/operator recovery workflows; the game does not yet include an importer that restores those snapshots. For production PostgreSQL operations, also use the provider's built-in point-in-time backups. - Create backups from the admin UI, the Tools CLI (
BlocksBeyondTheStars.Tools backup saves <world>), or on a schedule. The Tools CLI also honorsBBS_DATABASE_PROVIDER=postgresql+BBS_POSTGRES_CONNECTION_STRING. - The world is
seed + parameters + player edits: the procedural terrain is regenerated, only your changes are stored, keeping saves small.
Singleplayer launches the same dedicated server as a bundled child process bound to loopback, so there is no separate code path — what works solo works in multiplayer. The main menu's Host Game opens that exact path to friends: it raises the player cap, adds an optional join password, and announces the host's LAN address in-game.
The server can use the optional Python service in ai-backend/ for
dynamic flavour text: NPC greetings (personal speech bubbles at vendors and mission
boards), mission-board flavour text, occasional VEGA ship-AI banter, and
admin-generated missions (/ai <prompt> in chat). The game is fully playable without it —
every AI text has a localized scripted fallback (DE+EN), and with aiLevel = Off (the
default) the server never contacts the backend.
Running in Docker? The image bundles this backend and starts it automatically when you mount its
.env— you don't run the steps below by hand. See §10 → Optional AI text backend.
-
Start the backend (from a repo checkout; needs uv):
cd ai-backend uv run uvicorn app.main:app --host 127.0.0.1 --port 8077 -
(Optional) Configure an LLM provider. Copy
ai-backend/.env.exampletoai-backend/.envand set the OpenAI-compatible endpoint — LM Studio (self-hosted), OpenAI, or Claude, selected purely by env:BBTS_AI_BASE_URL=http://localhost:1234/v1 # e.g. LM Studio BBTS_AI_MODEL=local-model BBTS_AI_API_KEY=lm-studio # ignored by LM Studio
With no provider configured the backend still works and returns deterministic bilingual template text.
-
Point the game server at it in
config/server.json: setaiLeveltoSuggest(valid AI missions are stored as inactive drafts for admin review) orAuto(valid AI missions are published immediately);aiBackendUrldefaults tohttp://127.0.0.1:8077. Any level other thanOffalso enables the greeting / board text / VEGA banter endpoints. Restart the server after the change. -
Verify:
http://127.0.0.1:8077/healthshould return{ "status": "ok", "llm": true|false }(llmsays whether a model is configured). In-game, an admin can type/ai <prompt>in chat to generate a mission.
The C# server stays authoritative: AI mission plans are validated against the loaded content and reward counts are clamped; greetings are flavour only. Backend errors are logged and the game continues with the static text — it never crashes the server. Endpoint details: ai-backend/README.md; design: AI_MISSION_BACKEND.md.
Players can install the Windows client straight from the server's own web page — no manual zip hand-off. This uses Velopack (MIT) for the installer and updates.
Build the installer (needs a built client and the vpk CLI — auto-installed on first run):
./scripts/sync-velopack-libs.ps1 # ONCE: vendor the Velopack runtime into the client, then refresh Unity
./scripts/build-client.ps1 # build the Windows player (includes the update runtime)
./scripts/publish-client-installer.ps1 -ServeDir <your server install dir>That produces BlocksBeyondTheStars-win-Setup.exe plus an update feed
(releases.win.json + *-full.nupkg) and, with -ServeDir, copies them into the install's
clients/ folder so the API serves them. Add -Msi to also build the machine-wide WiX MSI.
The release version comes from
PlayerSettings.bundleVersion(the single source of truth) unless you pass-Version; local/dev builds carry0.1.0-dev, which Velopack accepts (it requirespackVersion >= 0.0.1). For public downloads you usually don't build by hand: pushing a git tagvX.Y.Zmakes CI publish a GitHub Release with the Setup.exe + MSI + Portable.zip — see DEVELOPER.md → Releases & versioning. The self-host portal below is the LAN/own-server channel; the two are complementary.
On the host: start BlocksBeyondTheStars.Api, and (for LAN/internet reach) bind it beyond loopback —
set adminBindAddress to the LAN IP or 0.0.0.0 and set an adminPassword first (§4). Only /api/*
is password-gated; /portal, /download, /download-linux, /download-mac and /updates stay public so players can reach them.
Players:
- Open
http://<server-ip>:<adminPort>/portal(default port 31416) in a browser. - Click Download the Windows client (served from
/download) and run the installer (per-user, no admin rights; an unsigned build shows a one-time SmartScreen "More info → Run anyway"). On Linux, click Download the Linux client (AppImage) (served from/download-linux), thenchmod +xthe.AppImageand run it. On macOS, click Download the macOS client (experimental) (served from/download-mac), unzip it and clear the Gatekeeper quarantine (xattr -dr com.apple.quarantine BlocksBeyondTheStars.app) since the build is unsigned. - Launch the game and Join the server's IP on the gameplay port (default 31415).
- Auto-update: in Settings → Software update, paste the update URL shown on the portal
(
http://<server-ip>:<adminPort>/updates) and use Check for updates. Publishing a higher version with the script above lets installed clients self-update from the server.
Updates only apply to an installed client (not a dev/Editor or portable-zip run). Each published version must be higher than the last.
The server can also serve the WebGL browser client at http://<server>:<adminPort>/play/. The portal's
Play in the browser button deep-links to it with the server host/port pre-filled (/play/?server_host=…&server_port=31415;
the slashless /play redirects there, keeping the query — the Unity page references its assets relatively, #218).
No download, no install — players just open the link, pick a pilot name and press Play (the deep-linked
menu hides the manual server picker, #221). Singleplayer/host are unavailable in the browser (it
only joins a hosted server over WebSocket), so BBS_ENABLE_WEBSOCKET=true is required.
The browser build is not baked into the server image (it needs Unity, which can't run in the image). You get it onto the server one of two ways:
- Mount a locally-built folder (works today): build the WebGL player on a machine with Unity
(
BlocksBeyondTheStars → Build WebGL Player, or headless-buildMethod …BuildScript.BuildWebGL), then bind-mountclient/Build/WebGLat/app/webgl(see the Docker section below). - Auto-fetch from a release (
BBS_FETCH_WEBGL=1): once a release ships awebgl*.zipasset, the entrypoint downloads and unzips it into/app/webgl. A mounted build always wins.
If no build is present, /play shows a friendly "not installed yet" page instead of a blank 404.
The TLS rule (read this before exposing it publicly): a browser will only open the gameplay WebSocket if the scheme is allowed for the page it is on.
| Where the page is reached | Gameplay WebSocket | Works? |
|---|---|---|
http://localhost:31416/play (same machine) |
ws://localhost:31415 |
✅ browsers exempt localhost |
http://<lan-ip>:31416/play (home LAN over http) |
ws://<lan-ip>:31415 |
✅ page is http, so ws:// is not mixed content |
https://<domain>/play (public, https) |
must be wss:// |
❌ unless the WebSocket has TLS — an https page cannot open a plain ws:// |
The server's WebSocket gateway has no built-in TLS, so public browser play needs a TLS-terminating
reverse proxy in front of the gameplay port. Use the ready-made Caddy setup
(docker-compose.tls.yml + docker/Caddyfile, see below) which auto-provisions a Let's Encrypt
certificate, or front it with Cloudflare Tunnel / a PaaS ingress (Fly.io, Railway, Azure Container Apps —
the latter is what the original Glitch web build used). localhost and LAN-over-http need none of this.
Where to host it (rough cost): you do not need Azure or any specific cloud, and SQLite (the default) is fine — no managed database required.
| Option | Cost (approx.) | TLS | Notes |
|---|---|---|---|
| Home PC/NAS + Cloudflare Tunnel | free (electricity) | free (Cloudflare) | the machine must stay on while people play |
| Small EU VPS (e.g. Hetzner/Netcup) | ~€4–6/month | free (Caddy + Let's Encrypt) | the typical hobby choice; docker-compose.tls.yml is built for this |
| PaaS (Fly.io / Railway) | free–~€10/month | free (managed) | an always-on WebSocket server may exceed free tiers |
| Azure Container Apps + managed PostgreSQL | ~€20–40+/month | free (managed ingress) | only worth it for a large always-on realm |
You can also host just the server on a cheap VPS and serve the static browser build for free elsewhere
(itch.io, GitHub Pages, Glitch) pointed at it with ?server_host=… — then you only pay for the server.
The dedicated server (game server and admin/portal/download UI, plus the optional AI text backend) can run as a single Linux container. This is optional, but a tagged release does build and publish the image to the GitHub Container Registry (GHCR), so you can just pull it:
docker pull ghcr.io/marceld23/blocks-beyond-the-stars-server:latest # or a :X.Y.Z version tagYou can also build it yourself (docker compose build / docker build) or trigger the standalone
Docker workflow from the Actions tab. The server is
Linux x64+ARM64 native, so the container runs on Linux, macOS, Windows (Docker Desktop / WSL2), a NAS
or a VPS. The game client stays Windows-only — the container hosts the server and hands the Windows
installer out via /download.
One image, asymmetric processes. Dockerfile publishes the headless projects
onto the .NET 10 ASP.NET runtime image and runs them through
docker/entrypoint.sh under tini (PID 1). The split is deliberate:
- the game server is the critical foreground process — it receives the shutdown signal and saves the world before exiting;
- the admin API is a best-effort sidecar — it auto-restarts and never takes the game server (and the players on it) down with it;
- the AI text backend (Python) is baked in but only started when you provide its
.env(below) — also a best-effort, auto-restarting sidecar.
docker stop sends SIGTERM, which the server handles directly: it drains and saves the world, then
exits 0. Give it time with a stop_grace_period/--stop-timeout of ~180 s — generous enough that even a
stop during the initial world generation of a brand-new world still ends in a clean save instead of a
SIGKILL.
Do not rewire the entrypoint to send SIGINT instead (issue #519): the game server runs as a shell
background job, POSIX has a non-interactive shell start those with SIGINT/SIGQUIT set to SIG_IGN, that
disposition survives exec, and .NET keeps an inherited SIG_IGN rather than installing its own handler.
A SIGINT sent from here is silently discarded and the container ends up SIGKILLed after the grace period
with everything since the last autosave lost. SIGTERM is the one signal a shell never takes away.
A quick end-to-end test on your own machine (Docker Desktop on Windows/macOS/Linux):
-
Run it (throwaway — no volumes, so the world is discarded on
rm):docker run -d --name bbts -p 31415:31415/udp -p 31416:31416/tcp \ -e BBS_ADMIN_PASSWORD=test123 -e BBS_SERVER_NAME="Local Test" \ ghcr.io/marceld23/blocks-beyond-the-stars-server:latestDocker pulls the image automatically. Add
-e BBS_FETCH_CLIENT=0to skip the GitHub client-installer download for a pure offline server test.To test a WebGL/browser client against the container, also publish the TCP/WebSocket gameplay port and enable the WebSocket listener:
docker run -d --name bbts-web \ -p 31415:31415/udp -p 31415:31415/tcp -p 31416:31416/tcp \ -e BBS_ENABLE_WEBSOCKET=true -e BBS_WEBSOCKET_BIND=+ \ -e BBS_ADMIN_PASSWORD=test123 -e BBS_SERVER_NAME="Local WebGL Test" \ ghcr.io/marceld23/blocks-beyond-the-stars-server:latest -
Check it's up — open the admin dashboard at http://localhost:31416/ and the public portal at http://localhost:31416/portal.
BBS_ADMIN_PASSWORDgates the/api/*calls (the dashboard prompts for it); the dashboard/portal pages themselves are public. -
Connect the game — launch the Windows client → Join → host
127.0.0.1, port31415. -
Watch it —
docker logs -f bbts, or in Docker Desktop → Containers →bbtsuse the Logs / Inspect / Exec tabs. -
Stop cleanly —
docker stop bbts(SIGTERM → the world is saved), thendocker rm bbts.
For a test that survives restarts, add the named volumes shown under Volumes.
docker compose up -d # build + run (see docker-compose.yml)
docker compose logs -f # follow the server log
docker compose down # SIGTERM -> clean world save, then stopConfigure with the BBS_* environment variables (see §3). At minimum set BBS_ADMIN_PASSWORD before
exposing the admin port. Ports: 31415/udp (native client), 31415/tcp (browser WebSocket, only
when BBS_ENABLE_WEBSOCKET=true), 31416/tcp (admin + portal + download).
Fail closed: the container binds the admin UI to
0.0.0.0. WithoutBBS_ADMIN_PASSWORDthe/apiadmin endpoints (config, backups, missions, content packs, logs) answer 401 on such a non-loopback bind — only the public pages (/portal,/play,/download*,/updates) stay up. Set the password (env var oradminPasswordinserver_config.json) to enable the dashboard.
docker build -t bbts-server .
docker run -d --name bbts \
-p 31415:31415/udp -p 31415:31415/tcp -p 127.0.0.1:31416:31416/tcp \
-e BBS_ADMIN_PASSWORD=change-me -e BBS_MAX_PLAYERS=12 \
-v bbts-saves:/app/saves -v bbts-config:/app/config -v bbts-clients:/app/clients \
--stop-timeout 60 bbts-server| Volume | Holds |
|---|---|
/app/saves |
SQLite world + backups/ + logs/ and /bump bug reports (<world>/bumps/) |
/app/config |
server.json (created on first run; env vars override it) |
/app/clients |
the published clients the portal serves: Windows *Setup.exe at /download + Linux *.AppImage at /download-linux + experimental macOS *-osx-*-Portable.zip at /download-mac |
/app/webgl |
the Unity WebGL browser build served at /play — bind-mount a local client/Build/WebGL, or let BBS_FETCH_WEBGL=1 fetch the release webgl*.zip |
To serve the in-browser client (§"Play in the browser"), put a WebGL build on the /app/webgl volume —
either bind-mount a locally-built client/Build/WebGL (-v "$PWD/client/Build/WebGL:/app/webgl:ro") or set
BBS_ENABLE_WEBSOCKET=true + BBS_FETCH_WEBGL=1 to auto-pull it from the release. Then open
http://localhost:31416/play. For a public deployment (https), use docker-compose.tls.yml — it adds a
Caddy reverse proxy that auto-provisions TLS so wss:// works (a plain docker run over the public internet
will fail the WebSocket on https). See the TLS table in §9.
BBS_DOMAIN=play.example.com BBS_ADMIN_PASSWORD=change-me \
docker compose -f docker-compose.tls.yml up -dSQLite (the default) needs no setup and is right for most self-hosted realms. For a larger/long-running
realm you can switch the authoritative world store to PostgreSQL with the overlay compose file — it adds
a postgres:16-alpine service and points the server at it (BBS_DATABASE_PROVIDER=postgresql +
BBS_POSTGRES_CONNECTION_STRING):
docker compose -f docker-compose.yml -f docker-compose.postgres.yml up -d
# public TLS: docker compose -f docker-compose.tls.yml -f docker-compose.postgres.yml up -dOverride POSTGRES_DB / POSTGRES_USER / POSTGRES_PASSWORD via a .env file or the shell. The world then
lives in Postgres (back it up with pg_dump); the saves volume still holds logs and /bump reports.
A Linux container can't build the clients, so on start the entrypoint pulls the newest *Setup.exe,
*.AppImage and the experimental *-osx-*-Portable.zip from the latest GitHub Release into
/app/clients (best-effort; each asset is fetched independently, so a missing one never blocks the
others — controlled by BBS_FETCH_CLIENT=1/0 and BBS_CLIENT_REPO). The portal (/portal) and the
one-click /download (Windows) / /download-linux (Linux AppImage) / /download-mac (macOS zip) routes
then work as in §9. Without them the portal still runs and the download routes report "nothing published
yet"; you can instead drop a Setup.exe / .AppImage / macOS zip into the clients volume yourself.
The in-game bug report / /bump feature works unchanged from a container: the client sends the
report (with its screenshot) over the network and the server writes a JSON snapshot (+ the JPG) to
saves/<world>/bumps/. Because that lives on the saves volume, reports survive restarts and are
retrievable from the host — browse the volume, or docker cp bbts:/app/saves/<world>/bumps ./bumps.
(Reports only divert to a repo's bugreports/server/ when the server runs from inside a git checkout,
which a normal container is not.)
The Python AI backend from §8 is bundled into the image (its own venv) but only starts when you configure it — otherwise no Python process runs. It is enabled when either:
- an
ai-backend/.envfile is mounted into the container at/app/ai-backend/.env(the normal way — copyai-backend/.env.exampleand fill in one provider), or - a
BBTS_AI_BASE_URLenvironment variable is set on the container.
Set BBS_AI_BACKEND=1/0 to force it on/off explicitly. When it starts it listens on the in-container
127.0.0.1:8077, which is the game server's default aiBackendUrl — so you only need to turn the
server's usage on with BBS_AI_LEVEL=Suggest (AI missions as drafts) or Auto (published);
any non-Off value also enables NPC greetings / board flavour / VEGA banter (see §8). With no .env
and BBS_AI_LEVEL=Off (the defaults), the game is fully AI-free and no backend runs.
# docker-compose.yml — enable the bundled AI backend
services:
server:
environment:
BBS_AI_LEVEL: "Suggest"
volumes:
- ./ai-backend/.env:/app/ai-backend/.env:roBundling LangChain/LangGraph makes the image noticeably larger. The AI backend is still entirely optional and the game runs identically without it; only the image size grows.
By default a self-hosted server never phones home: automatic crash reports are only written to the
local crashreports/ folder, and uploading stays off until you set an endpoint key. If you want your
players' server crashes collected in one place you control, run the standalone ReportHost
container (SQLite + screenshots, keyed read API, Basic-Auth admin UI) and point your game server at it:
# 1) run the inbox (see docker-compose.reports.yml / docs/developer/REPORT_HOST.md)
BBS_REPORTS_WRITE_KEY=my-write-key BBS_REPORTS_ADMIN_USER=me BBS_REPORTS_ADMIN_PASSWORD=secret \
docker compose -f docker-compose.reports.yml up -d
# 2) tell the game server to upload its crash reports there
# (docker-compose.yml → services.server.environment, or config/server.json)
BBS_CRASH_REPORT_ENDPOINT: "http://reports-host:31418/api/bugreport"
BBS_CRASH_REPORT_KEY: "my-write-key"Reports are then browsable at http://localhost:31418/admin (Basic Auth). The player-facing F1
feedback dialog is unaffected — it reports to the official developers regardless of which server the
player is on. Full endpoint/config reference: REPORT_HOST.md.