Standardized container images for game servers with KGSM compatibility
- Overview
- Supported Games
- Getting Started
- Image Architecture
- Building the Images
- Player-Presence Detection
- Adding a New Game Image
- Contributing
- License
KGSM-Containers provides standardized Docker container images for game servers that work seamlessly with KGSM (Krystal Game Server Manager). While designed for KGSM integration, these containers can also be used independently.
All game images derive from a single shared base image, kgsm-base, which carries the common toolchain, a non-root user, an init process, and an additive player-presence detector. Each game image adds only its game-specific dependencies and launch command.
| Game | Image |
|---|---|
| V Rising | vrising |
| Enshrouded | enshrouded |
| The Forest | theforest |
| Empyrion: Galactic Survival | empyrion |
| Abiotic Factor | abioticfactor |
| The Lord of the Rings: Return to Moria | lotrrtm |
Note
New game server images are added regularly. Contributions welcome — see Adding a New Game Image.
- Docker installed and running
- Basic knowledge of Docker commands
Note
Container-based instances are available with KGSM v2.0+.
# Create a container-based game server
kgsm --create vrising --name myserverKGSM wires up the volumes, ports, and (optionally) player-presence detection for you. See the KGSM repository.
docker run -it \
--name vrising-server \
-p 9876:9876/udp \
-p 9877:9877/udp \
-v vrising-backups:/opt/vrising/backups \
-v vrising-install:/opt/vrising/install \
-v vrising-logs:/opt/vrising/logs \
-v vrising-saves:/opt/vrising/saves \
-v vrising-temp:/opt/vrising/temp \
ghcr.io/thekrystalship/vrising:latestPlayer-presence detection stays off unless you provide its env (see Player-Presence Detection).
Images are built in two layers: the shared kgsm-base, and one image per game FROM kgsm-base:<version>.
A single foundation that every game image derives from. It contains:
- Base OS: Debian 13 (trixie), via
steamcmd/steamcmd:debian(SteamCMD preinstalled at/usr/bin/steamcmd). tinias PID 1 — clean signal forwarding sodocker stopshuts the game down gracefully.- Common game toolchain: Wine (
wine64),xvfb(virtual display), andlocales(en_US.UTF-8), with i386 enabled. - A non-root
kgsmuser (home/home/kgsm). /run/kgsm— the in-container event channel directory (owned bykgsm)./opt/kgsm/—entrypoint-base.sh(the wrapper) andkgsm-presence-shim.sh(the detector).
It sets the tini ENTRYPOINT and no CMD — each game supplies its own CMD.
tini (PID 1)
└─ /opt/kgsm/entrypoint-base.sh
├─ starts kgsm-presence-shim.sh in the background (non-fatal)
└─ exec <game CMD> ── stdout tee'd to /run/kgsm/game.log (the shim reads it)
The game's CMD is the per-game launch command — the same command that used to be the game's ENTRYPOINT. The base wrapper is purely additive: the game's stdout still reaches docker logs, and the per-game management script (including its stdin-FIFO stop/save path) runs unchanged.
Each game image adds only what is specific to that game: extra dependencies, its server directories, its management/launch script, EXPOSEd ports, USER kgsm, and a CMD. The shared user, toolchain, X11 setup, locale, init, and presence shim all come from kgsm-base.
/opt/<game>/
├── backups/ # automated or manual backups
├── install/ # game server installation files
├── logs/ # server log files
├── saves/ # game save files
├── temp/ # temporary files
└── <game>.manage.sh # management / launch script (the image CMD)
cd images
./build_all.sh # base first, then every game (add --push to publish)
# or individually:
cd base && ./build.sh # the base — build this before any game
cd ../vrising && ./build.shGame images pin a specific base tag — FROM kgsm-base:1.0 — never the floating kgsm-base / :latest. The version is defined once, in images/base/build.sh:
version="1.0" # base/build.sh — tags kgsm-base:1.0 and kgsm-base:latestSo rebuilding (or changing) the base produces a new kgsm-base:latest, but every game keeps sitting on the exact :1.0 it pinned until you deliberately move it.
A base bump is intentional and explicit — change both, together:
version="…"inimages/base/build.sh, and- the
FROM kgsm-base:<version>line in every game Dockerfile.
Then rebuild the base, then the games. Skipping either half is the failure mode the pin exists to prevent.
The full recipe (Dockerfile, entrypoint-base.sh, kgsm-presence-shim.sh, build.sh) lives in images/base/ and is committed to git, so ./images/base/build.sh reproduces the base.
- It is not bit-reproducible: the
steamcmd/steamcmd:debiantag and the apt package versions float. Pin the base by@sha256:digest and pin apt versions if you need exact reproducibility. - For a portable / offline copy, export the built image:
docker save kgsm-base:1.0 -o kgsm-base-1.0.tar # later: docker load -i kgsm-base-1.0.tar
Every image ships an additive shim (/opt/kgsm/kgsm-presence-shim.sh) that tails the game's log and self-reports player join/leave events as NDJSON to /run/kgsm/events.ndjson. The host bind-mounts that directory; KGSM's watchdog reads the file and turns the lines into engine events.
| Env var | Meaning |
|---|---|
KGSM_PLAYER_JOINED_REGEX_B64 |
base64-encoded regex matching a "player joined" log line |
KGSM_PLAYER_LEFT_REGEX_B64 |
base64-encoded regex matching a "player left" log line |
Each regex is a Perl pattern with optional named groups (?<id>…) and/or (?<name>…). Unset or empty → that detection is disabled — the shim stays out of the way and emits nothing. Presence is never faked: no pattern means no events, not a fabricated count.
{"type":"player_joined","id":"<id|null>","name":"<name|null>","ts":"<ISO-8601-UTC>"}
{"type":"player_left","id":"<id|null>","name":"<name|null>","ts":"<ISO-8601-UTC>"}A match that captures neither an id nor a name is skipped (never emitted as {null,null}). The file is created fresh on each container start.
The shim is strictly additive — it never reroutes the game's stdout and never touches the management script or its stop/save path.
Create images/<game>/ with a Dockerfile, a build.sh (copy an existing game's), and your management/launch script. The Dockerfile follows this pattern:
# Pin the shared base — never bare `FROM kgsm-base`.
FROM kgsm-base:1.0
EXPOSE 7777/udp 27015/udp
ARG DEBIAN_FRONTEND=noninteractive
ENV SERVER_HOME=/opt/<game> \
MANAGEMENT_FILE=/opt/<game>/<game>.manage.sh
# Game server directories (the kgsm user comes from the base).
RUN mkdir -p ${SERVER_HOME}/{backups,install,logs,saves,temp} \
&& chown -R ${USER}:${USER} ${SERVER_HOME}
# Only game-specific dependencies here — the common toolchain is in kgsm-base.
RUN apt-get update && apt-get install -y --no-install-recommends <pkgs> \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --chown=${USER}:${USER} --chmod=0755 ./<game>.manage.sh ${MANAGEMENT_FILE}
WORKDIR ${SERVER_HOME}
USER ${USER}
# Set CMD, NOT ENTRYPOINT — the base ENTRYPOINT (tini → wrapper → shim) runs this verbatim.
CMD ["/opt/<game>/<game>.manage.sh", "--start"]Key rules: pin the base version, add only game-specific deps, and set CMD (not ENTRYPOINT). To enable presence detection, supply the two regex env vars (KGSM does this from the blueprint).
We welcome new game server images! See CONTRIBUTING.md. Follow the Adding a New Game Image pattern above, or start from the skeleton in templates/.
This project is licensed under the GNU General Public License v3.0.