Dockerized BrainBread dedicated server running on Half-Life Dedicated
Server (HLDS). The Docker image provides the engine (downloaded via
SteamCMD at build time), while the BrainBread mod files are
bind-mounted at runtime from a local brainbread/ directory.
- Docker and Docker Compose
- A populated
brainbread/directory containing the server mod files (see below)
The brainbread/ directory is gitignored and must be populated before
the first run. It should contain the full server-side mod data: maps,
models, sounds, sprites, configs, and the server DLL (dlls/bb.so).
Download the latest Linux server package from ironoak.ch/BB and extract it in the repository root:
tar xzf brainbread-v1.3.37-linuxserver.tar.gzThis produces a brainbread/ directory with everything the server
needs, ready to go.
Alternatively, clone the BrainBread data repository directly:
git clone https://github.com/IronOak-Studios/BrainBread.git brainbreadThe repository includes the server DLL, maps, and all mod data.
docker compose buildOr without Compose:
docker build -t brainbread .The build is a two-stage Dockerfile that:
- Downloads HLDS (app 90) via SteamCMD
- Compiles
stat_fix.soandhealthcheck(see Technical notes) - Produces a minimal final image with just the HLDS runtime and helper binaries
The HLDS download is the slow step (~1.5 GB). Docker layer caching
means subsequent rebuilds (e.g. after editing stat_fix.c or
entrypoint.sh) skip the download.
docker compose up -dOr without Compose:
docker run -d \
-p 27015:27015/udp \
-p 27015:27015/tcp \
-v ./brainbread:/opt/hlds/brainbread \
brainbreadAll variables are optional and have sensible defaults. Set them in
docker-compose.yml under environment: or pass them with
docker run -e.
| Variable | Default | Description |
|---|---|---|
SERVER_NAME |
BrainBread v1.3.37 Server |
Hostname shown in the server browser |
MAP |
bb_chp1_heavensgate |
Starting map |
MAXPLAYERS |
12 |
Player slots (2-32) |
RCON_PASSWORD |
(empty -- RCON disabled) | Remote console password |
SERVER_PORT |
27015 |
Listen port |
EXTRA_ARGS |
(empty) | Additional hlds_linux arguments |
These are passed as command-line +args to hlds_linux by the
entrypoint script, and override matching values in server.cfg.
Server configuration lives in the bind-mounted brainbread/ directory.
The main files to edit:
brainbread/server.cfg-- Server cvars (hostname, timelimit, difficulty, experience settings, etc.). Executed on every map change.brainbread/mapcycle.txt-- Map rotation list.
Changes to these files take effect on the next map change or server restart -- no image rebuild required.
The Docker image only contains HLDS and the stat shim -- it does not need rebuilding for mod updates.
To update just the server DLL:
cp bb.so brainbread/dlls/
docker compose restartFor a full mod update, extract a new server package over the existing
directory. This overwrites binaries and mod assets but preserves any
custom configs (like server.cfg) that aren't in the archive:
tar xzf brainbread-v1.4-linuxserver.tar.gz
docker compose restartIf you set up via git clone, pull the latest changes instead:
git -C brainbread pull
docker compose restartThe entrypoint runs hlds_linux inside a restart loop. If the server
process crashes, it is restarted automatically after a 3-second delay.
A background watchdog probes the server every 60 seconds with a UDP A2A_PING query (the standard GoldSrc server ping). If the server fails to respond 3 times in a row (i.e. unresponsive for ~3 minutes), the watchdog kills the process, triggering a restart. The watchdog waits 120 seconds after each start before probing to allow time for map loading.
docker compose stop and docker stop send SIGTERM, which the
entrypoint traps to shut down the server gracefully and exit the
restart loop.
HLDS is a 32-bit application. When it calls stat() or readdir() on
files living on a filesystem with 64-bit inodes (overlayfs, bind
mounts, tmpfs -- common in containers), glibc returns EOVERFLOW
because the inode number doesn't fit in the 32-bit struct stat.
stat_fix.c compiles into a small shared library that intercepts
__xstat, __lxstat, and readdir, routing them through their 64-bit
counterparts (stat64, lstat64, readdir64) and truncating the
results back to 32-bit structs. The entrypoint script preloads it via
the 32-bit dynamic linker (ld-linux.so.2 --preload).
healthcheck.c compiles into a small 32-bit binary that sends a UDP
A2A_PING packet (0xFFFFFFFF69) to localhost on a given port and
waits for the server's A2A_ACK response. It exits 0 on success, 1 on
timeout. The watchdog in entrypoint.sh uses this to detect a hung
server without requiring any runtime dependencies beyond the binary
itself.
The Dockerfile uses two stages to keep the final image small. The
builder stage pulls in gcc-multilib, curl, and SteamCMD -- none of
which are needed at runtime. Only HLDS, the compiled shim, and minimal
32-bit runtime libraries are copied into the final image.