Beyond All Reason promotional battle banner — official BAR Media Promokit, beyondallreason.info/promokit.

Teiserver + autohost setup

Host Your Own Beyond All Reason Server | Complete Setup Guide

Step by step instructions

Use AI To Setup

If you have a ChatGPT etc. installed on your computer you can have it quickly set up a BAR server for you! You can even use LLM to install via SSH on a VPS remotely.

Provide details of where you want to set up a BAR server and then copy/paste this prompt for your LLM.

Its highly recommended to use Sol with high (ChatGPT/Codex) or Opus High (Claude) or better. Testing with Gemini, Deep Seek and Grok found they are unable to install without issues and manual troubleshooting, at which point its probably better to go the manual route.

Be sure to use planning mode to have it make a plan with you first, and once you review the plan enable auto/work mode and sit back while it does the rest.

Note: The manual step-by-step guide is below.

Show the full prompt text
You are setting up a self-hosted "Beyond All Reason" (BAR) game server for me. BAR is an open-source RTS built on the Spring/Recoil engine. A working server is two cooperating pieces of software:

1. Teiserver — the lobby/matchmaking/account/chat/moderation server (github.com/beyond-all-reason/teiserver). It cannot launch a match by itself.
2. SPADS — an autohost bot that logs into Teiserver as a normal bot account, launches the actual headless game engine process, and keeps a synced copy of the game content. BAR's config layer on top of generic SPADS lives at github.com/beyond-all-reason/spads_config_bar.

Everything runs in Docker Compose. Do not install Elixir, Erlang, Postgres, or Node directly on the host — they all run inside the Teiserver container's own multi-stage Dockerfile, which is required (a bare "mix release" skips the asset-compile step and produces a broken UI with missing CSS/JS).

FIRST, confirm the target environment with me before doing anything:
- Where is this running: my current machine, a rented VPS I'll SSH you into (DigitalOcean/Hetzner/Vultr/etc. — a $5-10/month box with 2GB+ RAM is enough), a second Linux machine on my network, or Windows via WSL2 (this path is untested — if you hit something WSL-specific, tell me rather than guessing)?
- Is this Ubuntu/Debian-based? Adjust package manager commands if not.
- Will this box also be my everyday desktop, or dedicated to the server? (Affects whether to set up boot-time auto-start.)
- LAN-only for now, or do you want it reachable from the public internet? Default to LAN-only unless I say otherwise — public exposure needs a firewall, TLS, and reverse-proxy work that should be a deliberate later step, not baked in blindly.
- What IP or domain should the server advertise to clients? (LAN IP like 192.168.1.x, or a public IP/domain if this is a VPS.)

Then do the following, explaining what you're doing at each step and pausing for my input at the marked decision points. Ask me before running anything destructive or before it needs sudo I don't have configured for you already.

## 1. Install Docker

Install Docker Engine + Compose plugin from Docker's OFFICIAL apt repository — not snap, not the distro's bundled docker.io package (both have known networking/permission quirks with this stack):

    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.gpg
    sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker "$USER"

The new docker group membership won't apply to your current shell session. Either start a fresh login shell, or prefix docker commands with `sg docker -c "..."` until I've logged out and back in.

## 2. Lay out the project and clone the repos

    mkdir -p ~/bar-server/{docker,data/postgres,repos}
    cd ~/bar-server/repos
    git clone --depth 1 https://github.com/beyond-all-reason/teiserver.git
    git clone --depth 1 https://github.com/beyond-all-reason/spads_config_bar.git

Clone the default branch with no tag pinned — that matches how BAR's own production actually runs (no release tag has been cut in years; main is what's live).

## 3. Generate secrets and write docker/.env

Generate real random secrets with openssl — never hand-type placeholder passwords into the real file:

    cd ~/bar-server/docker
    cat > .env <<EOF
    TEI_DB_HOSTNAME=127.0.0.1
    TEI_DB_NAME=teiserver_prod
    TEI_DB_USERNAME=teiserver_prod
    TEI_DB_PASSWORD=$(openssl rand -hex 16)
    TEI_HTTP_SECRET_KEY_BASE=$(openssl rand -base64 48)
    TEI_SETUP_ROOT_KEY=$(openssl rand -hex 24)
    TEI_DOMAIN_NAME=<the IP or domain I gave you>
    TEI_OAUTH_ISSUER=http://<the IP or domain I gave you>:4000
    TEI_NODE_NAME=barserver

    SPADS_LOBBY_LOGIN=BAR_SPADS_01
    SPADS_LOBBY_PASSWORD=$(openssl rand -hex 12)
    SPADS_OWNER_LOBBY_LOGIN=root
    SPADS_REGISTRATION_EMAIL=spads@example.com
    EOF

Two validation rules that are easy to trip on: the SPADS bot login must use an underscore, not a hyphen (a hyphen causes a login error), and its registration email must contain a dot in the domain part (a bare "@localhost"-style domain is rejected).

Add this file to .gitignore if this directory is ever put under git.

## 4. Write docker/docker-compose.yml

Put BOTH teiserver and spads on network_mode: "host" from the very start. This is important: if teiserver stays on Docker's default bridge network while spads uses host networking, Docker NAT-hairpins the autohost's connection to the lobby server, and the lobby server (which deliberately trusts only the raw observed TCP peer address, as an anti-spoof measure) ends up advertising the Docker bridge gateway address to joining clients instead of a real one. Login and lobby browsing work fine in that broken state — only the actual match connection silently fails after a ~30 second timeout with no obvious error. Avoid the whole bug by starting both services on host networking:

    services:
      teiserver:
        build:
          context: ../repos/teiserver
          dockerfile: Dockerfile
        container_name: bar-teiserver
        restart: unless-stopped
        network_mode: "host"
        depends_on:
          database:
            condition: service_healthy
          mailserver:
            condition: service_started
        env_file: [.env]
        environment:
          - PHX_SERVER=true
          - TEI_ENABLE_EMAIL_INTEGRATION=true
          - TEI_SMTP_SERVER=127.0.0.1
          - TEI_SMTP_HOSTNAME=127.0.0.1
          - TEI_SMTP_PORT=1025
          - TEI_SMTP_USERNAME=teitestuser
          - TEI_SMTP_PASSWORD=teitestpassword
          - TEI_SMTP_TLS_VERIFY=false

      database:
        image: postgres:17-alpine
        container_name: bar-teiserver-db
        restart: unless-stopped
        environment:
          - POSTGRES_DB=teiserver_prod
          - POSTGRES_USER=teiserver_prod
          - POSTGRES_PASSWORD=${TEI_DB_PASSWORD}
        volumes: ["../data/postgres:/var/lib/postgresql/data"]
        ports: ["127.0.0.1:5432:5432"]
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U teiserver_prod -d teiserver_prod"]
          interval: 5s
          timeout: 5s
          retries: 10

      mailserver:
        image: axllent/mailpit
        container_name: bar-mailpit
        restart: unless-stopped
        command: ["--smtp-tls-cert", "sans:localhost", "--smtp-tls-key", "sans:localhost"]
        environment:
          - MP_SMTP_AUTH_FILE=/tmp/auth
          - MP_SMTP_AUTH_ALLOW_INSECURE=true
        volumes: ["./containers/mailpit-auth/auth:/tmp/auth"]
        ports:
          - "127.0.0.1:8025:8025"
          - "127.0.0.1:1025:1025"

      spads:
        build:
          context: ../repos/spads_config_bar
        container_name: bar-spads
        restart: unless-stopped
        network_mode: "host"
        depends_on: [teiserver]
        env_file: [.env]
        environment:
          - SPRING_DATADIR=/spring-data
          - SPRING_WRITEDIR=/spring-data
          - RECOIL_ENGINE_VERSION=2025.06.24
          - SPADS_ARGS=--tls-cert-trust
          - SPADS_LOBBY_HOST=<the IP or domain I gave you>
          - SPADS_BASEGAME_PORT=53200
          - SPADS_BASEAUTOHOST_PORT=53100
          - SPADS_BATTLENAME_PREFIX=Self-Host -
          - SPADS_LAN_IP=<the IP or domain I gave you>
          - SPADS_MAX_INSTANCES=1
          - SPADS_TARGET_SPARES=1
          - SPADS_PLUGINS=AutoRegister;ClusterManager;JsonStatus;InGameMute;BarManager
          - SPRING_HOSTIP=<the IP or domain I gave you>
        volumes:
          - spring_data:/spring-data
          - spads_cache:/opt/spads/var
          - spring_engines:/spring-engines

      games-updater:
        build:
          context: ../repos/spads_config_bar
          dockerfile: Dockerfile.services
        container_name: bar-spads-games-updater
        restart: "no"
        profiles: ["tools"]
        command: /usr/bin/sync-all.sh
        environment:
          - SPRING_DATADIR=/spring-data
          - SPRING_WRITEDIR=/spring-data
          - RECOIL_ENGINE_VERSION=2025.06.24
          - PRD_RAPID_REPO_MASTER=https://repos-cdn.beyondallreason.dev/repos.gz
          - PRD_RAPID_USE_STREAMER=false
        volumes:
          - spring_data:/spring-data
          - spring_engines:/spring-engines

    volumes:
      spring_data:
      spads_cache:
      spring_engines:

Also create the mail-catcher auth file (must match TEI_SMTP_USERNAME/PASSWORD above):

    mkdir -p ~/bar-server/docker/containers/mailpit-auth
    HASH=$(openssl passwd -apr1 teitestpassword)
    echo "teitestuser:$HASH" > ~/bar-server/docker/containers/mailpit-auth/auth

A few values above are easy to get wrong if you go looking at other reference material instead of this prompt — use these exact ones:
- The lobby server's real Spring TCP listener defaults to port 8200, not 8000/8001 (an older example compose file mislabels this).
- Game content must come from BAR's own CDN, https://repos-cdn.beyondallreason.dev/repos.gz, using the byar:test rapid tag — NOT the generic repos.springrts.com master or a bar:test tag, which are a stale, unrelated community mirror.
- PRD_RAPID_USE_STREAMER=false is required because that CDN serves plain static files and doesn't implement the legacy streaming download endpoint pr-downloader tries by default (it 404s otherwise).
- Mailpit needs the TLS cert flags and auth file above, or account registration fails with a misleading Jason.Encoder/HTTP 500 error — the real cause is that Teiserver's mailer hardcodes tls: :always and auth: :always regardless of env vars, and stock Mailpit doesn't satisfy either by default. If you hit a registration 500 anyway: check whether the account row was already created in Postgres before retrying with the same username — the DB insert happens before the email-send step.

## 5. Bring the stack up

    cd ~/bar-server/docker
    docker compose up -d
    docker compose run --rm games-updater

Database migrations run automatically at container boot — don't run a manual migrate command. games-updater is a one-shot job excluded from the default up (via its tools profile); run it once now, and again in future whenever you deliberately bump RECOIL_ENGINE_VERSION or the content tag, since nothing here updates them automatically.

You do NOT need to manually create a bot account for SPADS in the admin panel. Its Docker image fully automates the SPADS install, and an AutoRegister plugin makes the bot self-register with Teiserver the first time it connects, using the SPADS_LOBBY_LOGIN/SPADS_LOBBY_PASSWORD already in .env. Confirm it worked with `docker compose logs spads --tail 80`.

## 6. Create the admin account

With TEI_SETUP_ROOT_KEY set, find and visit the setup route that consumes it (check lib/teiserver_web/controllers/account/setup_controller.ex in the cloned repo for the exact current path — it has moved between versions) to bootstrap a root@localhost account, whose password becomes that key's value.

Log in as root, then IMMEDIATELY enroll TOTP at /teiserver/account/security/totp/edit, before trying any admin page. Every privileged role (Admin, Moderator, Server, Overwatch, Contributor) is hard-gated on having TOTP configured — a fresh account without it gets a bare, unhelpful "Unauthorized" on every privileged action with no explanation of why.

Register my real account through the normal /register page, then, from the root session, grant it the Admin role via the admin user page, and enroll TOTP for it too.

One non-obvious role fact: Server is a SUPERSET of Admin, not the other way around. Holding Admin alone will NOT unlock Server-gated pages like telemetry or several moderation reports — that's the opposite of what you'd naturally assume. There is no UI path to grant Server directly; do it via SQL:

    docker exec bar-teiserver-db psql -U teiserver_prod -d teiserver_prod -c \
      "UPDATE account_users SET roles = array_append(roles, 'Server') WHERE name = 'MYUSERNAME' AND NOT ('Server' = ANY(roles));"

Two gotchas with this: Teiserver caches user rows in memory (ETS), so a raw SQL edit can be silently clobbered the next time the app updates that same row on its own — prefer the app's own update functions when practical, and re-check the role stuck afterward. And if a role change doesn't seem to show up in the UI even after it's confirmed in the database, do a full page reload / re-login rather than assuming the grant failed — an already-open LiveView session only loads current_user once, at socket-mount time, and won't reflect a mid-session DB change until it's torn down and reopened.

Reach the containerized Postgres only via `docker exec bar-teiserver-db psql ...` — never via a host-level `psql -h localhost`, which is very likely to silently hit a completely unrelated Postgres instance already running on the machine instead of this one.

## 7. Verify with a real match

Confirm the autohost shows up as connected/registered (`docker compose logs spads`), then connect with the actual BAR game client from a SEPARATE machine — never install or run the game client on the server host itself, since a client and server sharing one machine's own network stack can mask exactly the kind of connectivity bug this stack is prone to. Play one full match end-to-end before calling this done.

## 8. Before exposing this beyond your LAN

Only do this section if I've told you I want public/internet access, not just LAN. Ask me to confirm before doing any of this:
- Enable and actually configure a firewall (ufw or equivalent) — a fresh Ubuntu install commonly has one present but DISABLED, which means nothing is filtering inbound traffic until it's turned on.
- Put a reverse proxy and TLS termination in front of the web/admin port (4000).
- Rotate every secret generated above, and any placeholder account password, before treating this as more than a private test.
- Double check you haven't left any local-debugging rate-limit relaxations in place — they should never carry into a public-facing deployment.
- Consider (all optional, and none configured by default because they need external services/API keys I may not have): an email domain blocklist for registration, a VPN/IP-reputation check via a third-party API, and requiring game-client hardware data before play (this makes the automatic MAC-hash smurf-ban system, which is already present in the code, actually effective).
- Think specifically about how the lobby server will observe the autohost's source IP in whatever hosting environment this is (cloud VPS NAT/load-balancer setups can reintroduce the exact same hairpinning-style bug described in step 4, even without Docker's bridge network being the cause) — verify a real match again after any networking change here, don't just assume it still works.

## 9. Ongoing maintenance

There is no automatic update mechanism for the game engine or content version — RECOIL_ENGINE_VERSION and the rapid tag in .env/compose are pinned values you (or I) update deliberately, then re-run `docker compose run --rm games-updater`. Docker's restart: unless-stopped brings containers back after a crash or Docker daemon restart, but starting the whole stack automatically at OS boot still requires Docker's own systemd service to be enabled — check `systemctl is-enabled docker` and set up a boot-start mechanism if I want that.

When you're done, report back to me: the URL to reach the web UI, the root setup-key login URL you used, confirmation that `docker compose ps` shows all containers healthy, and the exact IP/domain value you configured everywhere above, so I can note it down.

Manual Installation

  1. Requirements
  2. Install Docker
  3. Get the server software
  4. Set passwords and config
  5. Write the Compose file
  6. Start it and sync game content
  7. Create your admin account
  8. Play a test match from another PC
  9. Lock it down before going public
  10. Keep it maintained

Read First

  • Change These: Any value shown in red is a placeholder — replace it with your own value before use.
  • Port 8200: The lobby server's real Spring TCP listener defaults to port 8200, not 8000.
  • SPADS Login: The bot login must use an underscore (e.g., BAR_SPADS_01). A hyphen will cause a login error.
  • SPADS Email: The bot registration email must contain a dot in the domain part (e.g., spads@bar.local). Bare localhost domains are rejected.
  • Host Networking: Both teiserver and spads must use network_mode: "host". If not, the autohost's connection gets NAT-hairpinned, and matches will silently fail to connect.
  • Content CDN: Game content must come from repos-cdn.beyondallreason.dev using the byar:test rapid tag, NOT the generic SpringRTS mirror.

1. Requirements

A spare Linux machine (Ubuntu or similar) or a Virtual Private Server (VPS) ($5-10/month, 2GB+ RAM) — your own, a spare box, or a rented one. Budget roughly 30 minutes.

2. Install Docker

  1. 2.1 Install Docker Engine and Compose, from Docker's own apt repository (not the distro-bundled package):
    sudo install -m 0755 -d /etc/apt/keyrings
    curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /tmp/docker.gpg
    sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg /tmp/docker.gpg
    sudo chmod a+r /etc/apt/keyrings/docker.gpg
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
    sudo apt-get update
    sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
    sudo usermod -aG docker "$USER"
  2. 2.2 Log out and back in before running docker without sudo — group changes don't apply to an already-open session.

3. Get the software

  1. 3.1 Clone both repos:
    mkdir -p ~/bar-server/{docker,data/postgres,repos}
    cd ~/bar-server/repos
    git clone --depth 1 https://github.com/beyond-all-reason/teiserver.git
    git clone --depth 1 https://github.com/beyond-all-reason/spads_config_bar.git
  2. 3.2 Both are cloned at their default branch — no tag pinned, matching how BAR's own production runs.

4. Set passwords and config

  1. 4.1 Generate secrets and write them to a git-ignored docker/.env file:
    cd ~/bar-server/docker
    cat > .env <<EOF
    TEI_DB_HOSTNAME=127.0.0.1
    TEI_DB_NAME=teiserver_prod
    TEI_DB_USERNAME=teiserver_prod
    TEI_DB_PASSWORD=$(openssl rand -hex 16)
    TEI_HTTP_SECRET_KEY_BASE=$(openssl rand -base64 48)
    TEI_SETUP_ROOT_KEY=$(openssl rand -hex 24)
    TEI_DOMAIN_NAME=<your-server-lan-or-public-ip>
    TEI_OAUTH_ISSUER=http://<your-server-lan-or-public-ip>:4000
    TEI_NODE_NAME=barserver
    
    SPADS_LOBBY_LOGIN=BAR_SPADS_01
    SPADS_LOBBY_PASSWORD=$(openssl rand -hex 12)
    SPADS_OWNER_LOBBY_LOGIN=root
    SPADS_REGISTRATION_EMAIL=spads@example.com
    EOF
  2. 4.2 The SPADS bot's login needs an underscore, not a hyphen.
  3. 4.3 Its registration email needs a dot in the domain — both are rejected otherwise.

5. Write the Compose file

  1. 5.1 Put both teiserver and spads on host networking from the start. Otherwise the autohost's connection gets NAT-hairpinned — the lobby server sees it arriving from an internal Docker address instead of a real one, and advertises that unreachable address to joining clients. Logins work fine; matches silently fail to connect. Host networking from the start avoids this.
  2. 5.2 Write docker-compose.yml:
    services:
      teiserver:
        build:
          context: ../repos/teiserver
          dockerfile: Dockerfile
        container_name: bar-teiserver
        restart: unless-stopped
        network_mode: "host"
        depends_on:
          database:
            condition: service_healthy
          mailserver:
            condition: service_started
        env_file: [.env]
        environment:
          - PHX_SERVER=true
          - TEI_ENABLE_EMAIL_INTEGRATION=true
          - TEI_SMTP_SERVER=127.0.0.1
          - TEI_SMTP_HOSTNAME=127.0.0.1
          - TEI_SMTP_PORT=1025
          - TEI_SMTP_USERNAME=teitestuser
          - TEI_SMTP_PASSWORD=teitestpassword
          - TEI_SMTP_TLS_VERIFY=false
    
      database:
        image: postgres:17-alpine
        container_name: bar-teiserver-db
        restart: unless-stopped
        environment:
          - POSTGRES_DB=teiserver_prod
          - POSTGRES_USER=teiserver_prod
          - POSTGRES_PASSWORD=${TEI_DB_PASSWORD}
        volumes: ["../data/postgres:/var/lib/postgresql/data"]
        ports: ["127.0.0.1:5432:5432"]
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U teiserver_prod -d teiserver_prod"]
          interval: 5s
          timeout: 5s
          retries: 10
    
      mailserver:
        image: axllent/mailpit
        container_name: bar-mailpit
        restart: unless-stopped
        command: ["--smtp-tls-cert", "sans:localhost", "--smtp-tls-key", "sans:localhost"]
        environment:
          - MP_SMTP_AUTH_FILE=/tmp/auth
          - MP_SMTP_AUTH_ALLOW_INSECURE=true
        volumes: ["./containers/mailpit-auth/auth:/tmp/auth"]
        ports:
          - "127.0.0.1:8025:8025"
          - "127.0.0.1:1025:1025"
    
      spads:
        build:
          context: ../repos/spads_config_bar
        container_name: bar-spads
        restart: unless-stopped
        network_mode: "host"
        depends_on: [teiserver]
        env_file: [.env]
        environment:
          - SPRING_DATADIR=/spring-data
          - SPRING_WRITEDIR=/spring-data
          - RECOIL_ENGINE_VERSION=2025.06.24
          - SPADS_ARGS=--tls-cert-trust
          - SPADS_LOBBY_HOST=<your-server-lan-or-public-ip>
          - SPADS_BASEGAME_PORT=53200
          - SPADS_BASEAUTOHOST_PORT=53100
          - SPADS_BATTLENAME_PREFIX=Self-Host -
          - SPADS_LAN_IP=<your-server-lan-or-public-ip>
          - SPADS_MAX_INSTANCES=1
          - SPADS_TARGET_SPARES=1
          - SPADS_PLUGINS=AutoRegister;ClusterManager;JsonStatus;InGameMute;BarManager
          - SPRING_HOSTIP=<your-server-lan-or-public-ip>
        volumes:
          - spring_data:/spring-data
          - spads_cache:/opt/spads/var
          - spring_engines:/spring-engines
    
      games-updater:
        build:
          context: ../repos/spads_config_bar
          dockerfile: Dockerfile.services
        container_name: bar-spads-games-updater
        restart: "no"
        profiles: ["tools"]
        command: /usr/bin/sync-all.sh
        environment:
          - SPRING_DATADIR=/spring-data
          - SPRING_WRITEDIR=/spring-data
          - RECOIL_ENGINE_VERSION=2025.06.24
          - PRD_RAPID_REPO_MASTER=https://repos-cdn.beyondallreason.dev/repos.gz
          - PRD_RAPID_USE_STREAMER=false
        volumes:
          - spring_data:/spring-data
          - spring_engines:/spring-engines
    
    volumes:
      spring_data:
      spads_cache:
      spring_engines:
  3. 5.3 Watch for these: the lobby server's real TCP port is 8200, not 8000. Game content comes from BAR's own Content Delivery Network (CDN) at repos-cdn.beyondallreason.dev with the byar:test tag, not the unrelated repos.springrts.com mirror. PRD_RAPID_USE_STREAMER=false is required because that CDN serves static files, not the legacy streaming endpoint.
  4. 5.4 Create the mail catcher's auth file, matching the SMTP credentials above:
    mkdir -p ~/bar-server/docker/containers/mailpit-auth
    HASH=$(openssl passwd -apr1 teitestpassword)
    echo "teitestuser:$HASH" > ~/bar-server/docker/containers/mailpit-auth/auth

6. Start it up

  1. 6.1 Start the stack and sync game content:
    cd ~/bar-server/docker
    docker compose up -d
    docker compose run --rm games-updater
  2. 6.2 Expected output: Database migrations run automatically at container boot. You should see no errors, and you do not need to run a manual migrate command.
  3. 6.3 games-updater runs on demand, not continuously; re-run it whenever you change the pinned engine or content version.
  4. 6.4 You don't need to create a bot account manually — the SPADS image's AutoRegister plugin registers it with the lobby server on first connect, using the credentials already in .env.

7. Create your admin account

  1. 7.1 With TEI_SETUP_ROOT_KEY set, visit the setup route with that key to create a bootstrap root@localhost account, using the key as its password.
  2. 7.2 Log in as root and immediately enable a Time-based One-Time Password (TOTP) at /teiserver/account/security/totp/edit — every privileged role requires it, and the admin panel just says "Unauthorized" with no explanation until it's set up.
  3. 7.3 Register your own account at /register, grant it Admin from the root session, and enable a Time-based One-Time Password (TOTP) for it too.
  4. 7.4 One catch: Server is a superset of Admin, not the reverse. Admin alone won't unlock Server-gated pages like telemetry. There's no UI for granting Server — it needs a direct database update:
    docker exec bar-teiserver-db psql -U teiserver_prod -d teiserver_prod -c \
      "UPDATE account_users SET roles = array_append(roles, 'Server') WHERE name = 'yourname' AND NOT ('Server' = ANY(roles));"
  5. 7.5 Where possible, prefer the app's own update paths over raw SQL — Teiserver caches user records in memory, and a raw column edit can get silently overwritten later.

Checkpoint: If you get an unhelpful "Unauthorized" error clicking around the admin panel, it means you haven't enabled TOTP. This is strictly required.

8. Play a test match from another PC

  1. 8.1 Confirm the autohost has joined the lobby.
  2. 8.2 Download and install the Beyond BAR launcher from the site's homepage on a separate machine — never run the game client on the server itself, since that can mask connection problems real players would hit. You'll need to enter your own server's address in the launcher's connection settings before it can find your lobby.
  3. 8.3 Play a full match before calling it done.

Checkpoint: The autohost shows as connected in logs, and a client from another network/machine connects, launches the engine, and loads the map successfully.

9. Lock it down before going public

As written, this is safe for LAN or invite-only use. Before opening it to the internet:

  1. 9.1 Enable a firewall (ufw or equivalent) — it's usually installed but off by default.
  2. 9.2 Rotate every generated secret and placeholder password.
  3. 9.3 Put a reverse proxy and TLS in front of the admin port if it's public-facing.
  4. 9.4 Undo any rate-limit relaxations made for local debugging.
  5. 9.5 Consider optional hardening: an email domain blocklist, a VPN/IP-reputation check, requiring client hardware data before play.

See host security and host operations for practices that apply beyond this specific setup.

10. Keeping it running

  1. 10.1 Nothing here auto-updates the game or engine version — RECOIL_ENGINE_VERSION and the content tag are pinned values you bump manually, then re-run games-updater.
  2. 10.2 Docker's restart: unless-stopped brings containers back after a crash, but starting at boot still needs Docker's own service enabled at the OS level.

See host operations for backups and monitoring, and host troubleshooting if something won't connect.

The directory is a community list of self-hosted servers where players can find games. Submitting is optional. Once your server is stable, submit it to the directory.

Useful Commands

  • Check Status:
    docker compose ps
  • View SPADS Logs:
    docker compose logs spads --tail 80
  • Restart Teiserver:
    docker compose restart teiserver
  • Stop Stack:
    docker compose down
  • Sync/Bump Game Content: Change the pinned version in .env, then run:
    docker compose run --rm games-updater