No description
  • TypeScript 94.2%
  • Dockerfile 3.5%
  • Shell 1.5%
  • CSS 0.4%
  • HTML 0.4%
Find a file
2026-06-21 18:09:00 +02:00
data Initial project 2026-06-21 12:25:29 +02:00
packages Instant logout, hand cursor over buttons 2026-06-21 12:54:47 +02:00
spike/gzdoom Added GZDoom support 2026-06-21 18:09:00 +02:00
.env.example Instant logout, hand cursor over buttons 2026-06-21 12:54:47 +02:00
.gitignore Initial project 2026-06-21 12:25:29 +02:00
docker-compose.yml Instant logout, hand cursor over buttons 2026-06-21 12:54:47 +02:00
LICENSE Initial commit 2026-06-21 12:22:10 +02:00
package-lock.json Initial project 2026-06-21 12:25:29 +02:00
package.json Initial project 2026-06-21 12:25:29 +02:00
README.md Instant logout, hand cursor over buttons 2026-06-21 12:54:47 +02:00

doom-mgmt

A self-hosted web app for creating, configuring, and running Zandronum (multiplayer Doom) servers as Docker containers — entirely from your browser. Upload your WADs and mods, build a server with point-and-click settings, and start it. Servers register with the master server so they show up in Doomseeker.

No more hand-writing server.cfg files or remembering long zandronum-server command lines.


Features

  • 🎮 Server builder — pick an IWAD + ordered PWADs, game mode, player limits, passwords, dmflags, and a map rotation, with a live server.cfg preview as you edit.
  • 📦 WAD/mod library — drag-and-drop upload of .wad / .pk3 files with content-hash dedupe and automatic IWAD/PWAD tagging.
  • 🐳 One container per server, spawned on demand through the Docker socket; start / stop / restart / delete from the dashboard.
  • 📜 Live logs — tail a server's container output in real time (SSE).
  • 🔐 Multi-user — first run prompts you to create the admin account; admins manage everyone else from a Users page.
  • 🚪 Registration toggle — public self-service signup is off by default (ALLOW_REGISTRATION); flip it on if you want open registration.

Architecture

┌─────────────────── Docker host ───────────────────┐
│                                                    │
│  docker compose (the management app)               │
│   ├─ web   React + Vite + Tailwind (nginx)         │
│   │          serves the UI, proxies /api ──┐       │
│   ├─ api   Node + Fastify + dockerode  <───┘       │
│   │          mounts /var/run/docker.sock           │
│   │          mounts ./data (wads + configs)        │
│   └─ db    Postgres (volume)                       │
│                                                    │
│  game servers (created by api via the Docker API)  │
│   ├─ zandronum-srv-<id>   network: host            │
│   │     /wads    ← shared wad dir (read-only)      │
│   │     /configs ← generated <id>.cfg              │
│   └─ … one container per server, each its own port │
└────────────────────────────────────────────────────┘
Service Stack Role
web React, Vite, Tailwind, nginx UI; reverse-proxies /api to the backend
api Node, Fastify, dockerode, Drizzle REST API; spawns/controls game containers
db Postgres users, wads, servers

Each game server runs the rcdailey/zandronum-server image with network_mode: host and a unique UDP port, mounting the shared data/wads (read-only) and data/configs directories:

zandronum-server -host -port <udp> -iwad /wads/doom2.wad \
  -file /wads/mod.pk3 +exec /configs/<server-id>.cfg

The code that turns a server's settings into that .cfg lives in packages/shared and is used by both the API (to write the file) and the web UI (for the live preview), so the two can never drift.


Quick start

Requirements: a host with Docker + the Compose plugin. (For multiplayer visibility you'll also want UDP ports reachable — see Networking.)

git clone <your-repo-url> doom-mgmt
cd doom-mgmt

cp .env.example .env
# Edit .env — at minimum set a strong JWT_SECRET and POSTGRES_PASSWORD
# (keep DATABASE_URL's password in sync). Generate a secret with:
#   node -e "console.log(require('crypto').randomBytes(48).toString('hex'))"

docker compose up --build

Open http://localhost:8080. On first run you'll be prompted to create the admin account (this always works, even with registration disabled).

Run from the repo root. The API talks to the host Docker daemon and the game containers bind-mount host paths. HOST_DATA_DIR defaults to ${PWD}/data in docker-compose.yml, so launching from elsewhere requires setting HOST_DATA_DIR to the absolute path of this repo's data/ directory.

Accounts & registration

  • The first account created becomes the admin.
  • After that, public self-service registration is controlled by ALLOW_REGISTRATION (default false). When off, the login page has no "Register" link and admins create accounts (with role user or admin) from the Users page.
  • Set ALLOW_REGISTRATION=true to let anyone sign themselves up.

IWADs

IWADs (e.g. doom2.wad) contain copyrighted game data and are not shipped. Upload your own in the WAD Library, or use the free Freedoom IWADs (freedoom1.wad / freedoom2.wad).

Networking

Open the firewall for:

  • the web UI — TCP WEB_PORT (default 8080), and
  • the game servers — the UDP range SERVER_PORT_MINSERVER_PORT_MAX (default 1066610766).

Zandronum's netcode strongly prefers host networking, which is why each server gets its own UDP port on the host rather than a bridged port mapping.


Configuration

All configuration is via environment variables (see .env.example):

Variable Default Description
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB doom / — / doommgmt Postgres credentials
DATABASE_URL API connection string (keep credentials in sync with the above)
JWT_SECRET Required. Secret used to sign session cookies
NODE_ENV development Set to production behind HTTPS so cookies are marked Secure
ALLOW_REGISTRATION false Allow public self-service registration
API_PORT 4000 Port the API listens on (internal to the compose network)
WEB_PORT 8080 Host port the web UI is published on
ZANDRONUM_IMAGE rcdailey/zandronum-server:official-latest Image used for game servers. Note: this repo has no latest tag — use a prefixed tag like official-latest, official-3.2.1, or tspg-latest
SERVER_PORT_MIN / SERVER_PORT_MAX 10666 / 10766 UDP port range allocated to servers
MAX_UPLOAD_MB 2048 Max size of a single WAD/mod upload (enforced by both nginx and the API)
DATA_DIR ./data Host directory for uploaded wads, generated cfgs, and the DB volume

Local development

Run the database in Docker and the app on the host with hot reload:

npm install
npm run build:shared          # build the shared types package once

docker compose up -d db       # Postgres only
# Point DATABASE_URL at localhost (e.g. postgres://doom:...@localhost:5432/doommgmt)

npm run dev:api               # Fastify on :4000 (tsx watch)
npm run dev:web               # Vite on :5173, proxies /api -> :4000
Command Description
npm run build:shared Build the shared package (required before dev:api / dev:web)
npm run dev:api Run the API with file watching
npm run dev:web Run the Vite dev server
npm run build Production build of all three packages
npm run typecheck Type-check all three packages

The API needs access to a Docker daemon (/var/run/docker.sock) to start game servers. WAD uploads, the cfg builder, and auth all work without one.


Project layout

packages/
  shared/   shared TS types · game-mode & dmflags tables · the cfg generator
  api/      Fastify backend
    auth/     register/login, JWT cookies, role guards, first-run setup
    admin/    admin-only user management
    wads/     upload (multipart, hashed, deduped), list, delete
    servers/  CRUD · port allocation · cfg writing
    docker/   dockerode wrapper (launch/stop/restart/remove/logs/status)
    db/       Drizzle schema + idempotent init
  web/      React frontend (pages, components, typed API client)
data/       runtime: wads/, configs/, pg/  (gitignored)
docker-compose.yml

API overview

All endpoints are served under /api (nginx strips the prefix in production).

Method Path Auth Purpose
GET /auth/setup public First-run + registration status
POST /auth/register public¹ Create account (first one = admin)
POST /auth/login · /auth/logout public Session management
GET /auth/me user Current user
GET/POST /admin/users admin List / create users
DELETE /admin/users/:id admin Delete a user
GET/POST /wads user List / upload WADs
PATCH/DELETE /wads/:id user Retag / delete a WAD
GET/POST /servers user List / create servers
GET/PUT/DELETE /servers/:id owner/admin Read / update / delete
POST /servers/:id/start · /stop · /restart owner/admin Lifecycle
GET /servers/:id/logs owner/admin SSE log stream
POST /servers/cfg-preview user Render cfg without saving

¹ /auth/register is closed once an admin exists unless ALLOW_REGISTRATION=true.


Roadmap

  • Live player count / server query (Zandronum UDP protocol)
  • Per-user server quotas
  • A first-party Zandronum image
  • Multi-host scheduling

License

MIT — see LICENSE (add one if you intend to publish).