- TypeScript 94.2%
- Dockerfile 3.5%
- Shell 1.5%
- CSS 0.4%
- HTML 0.4%
| data | ||
| packages | ||
| spike/gzdoom | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
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.cfgfiles or remembering longzandronum-servercommand lines.
Features
- 🎮 Server builder — pick an IWAD + ordered PWADs, game mode, player
limits, passwords, dmflags, and a map rotation, with a live
server.cfgpreview as you edit. - 📦 WAD/mod library — drag-and-drop upload of
.wad/.pk3files 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_DIRdefaults to${PWD}/dataindocker-compose.yml, so launching from elsewhere requires settingHOST_DATA_DIRto the absolute path of this repo'sdata/directory.
Accounts & registration
- The first account created becomes the admin.
- After that, public self-service registration is controlled by
ALLOW_REGISTRATION(defaultfalse). When off, the login page has no "Register" link and admins create accounts (with roleuseroradmin) from the Users page. - Set
ALLOW_REGISTRATION=trueto 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(default8080), and - the game servers — the UDP range
SERVER_PORT_MIN–SERVER_PORT_MAX(default10666–10766).
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).