Open-source deployment bundle for running an Enshrouded self-hosted server or Enshrouded dedicated server on a spare PC, with Docker, or on Kubernetes.
This repo is aimed at operators who want a self-hosted Enshrouded server with multiple deployment options: bare Linux, Docker Compose, plain Kubernetes manifests, and a Helm chart.
This repo includes:
- a game-server image in
image/ - an optional lightweight stats API in
tools/services/api/ - Docker Compose, plain Kubernetes manifests, and a Helm chart
- a bare-Linux installer for running the server as a user systemd service
- replayable probe/API tests in
tools/tests/replay/
Use this repo if you want to:
- deploy an Enshrouded self-hosted server with Docker Compose
- run an Enshrouded dedicated server on Kubernetes with Helm or plain manifests
- host an Enshrouded server on a spare Linux machine without containers
- expose simple live metrics and Discord webhook notifications for an Enshrouded server
If you are here to install an Enshrouded server, start with one of these:
- Install With Docker Compose
- Install On Kubernetes With Plain Manifests Or Kustomize
- Install On Kubernetes With Helm
- Install On Bare Linux With systemd
ghcr.io/shipstuff/enshrouded-serverghcr.io/shipstuff/enshrouded-live-stats-api
Published public Helm chart:
oci://ghcr.io/shipstuff/charts/enshrouded
The Helm chart is validated on pull requests and main, and published to GHCR as an OCI chart on version tags like v0.1.0. The git tag must match Chart.yaml version.
Use the published images:
docker compose up -dBuild locally instead:
docker compose up -d --buildOverride runtime defaults through shell env or a local .env file:
SERVER_NAME="My Enshrouded Server" \
SAVE_IMPORT_MODE=1 \
SAVE_IMPORT_BIND=127.0.0.1 \
docker compose up -dSteamCMD update/validate runs before every server launch by default. Set
AUTO_UPDATE_ON_BOOT=0 if you need startup to skip network update checks.
Published-image overrides:
ENSHROUDED_IMAGE=ghcr.io/your-org/enshrouded-server:latest \
ENSHROUDED_STATS_API_IMAGE=ghcr.io/your-org/enshrouded-live-stats-api:latest \
docker compose up -dDefault exposed ports:
- UDP
15637 - UDP
27015 - TCP
8091for the stats API - TCP
8080only matters whenSAVE_IMPORT_MODE=1
Apply the included manifests:
kubectl apply -f namespace.yaml
kubectl apply -f pvc.yaml
kubectl apply -f statefulset.yaml
kubectl apply -f service.yamlOr use the repo-root Kustomize wrapper:
kubectl apply -k .The plain manifest defaults point at GHCR. If you publish under your own registry, update the image fields in statefulset.yaml or patch them after apply:
kubectl -n games set image statefulset/enshrouded \
enshrouded=ghcr.io/your-org/enshrouded-server:latest \
enshrouded-stats-api=ghcr.io/your-org/enshrouded-live-stats-api:latestThe included StatefulSet also binds the game/query UDP ports directly on the node with hostPort:
15637/udp27015/udp
Install with the public defaults:
helm upgrade --install enshrouded ./helm/enshrouded \
--namespace games --create-namespaceInstall the published OCI chart instead:
helm upgrade --install enshrouded oci://ghcr.io/shipstuff/charts/enshrouded \
--version 0.1.0 \
--namespace games --create-namespaceTypical overrides:
helm upgrade --install enshrouded ./helm/enshrouded \
--namespace games --create-namespace \
--set image.repository=ghcr.io/your-org/enshrouded-server \
--set statsApi.image.repository=ghcr.io/your-org/enshrouded-live-stats-api \
--set image.tag=0.1.0 \
--set statsApi.image.tag=0.1.0The chart also supports:
imagePullSecrets- disabling the sidecar API with
--set statsApi.enabled=false - enabling the save-import UI service with
--set service.saveImport.enabled=true - disabling startup SteamCMD update/validate with
--set serverUpdate.autoUpdateOnBoot=false - toggling direct node UDP bindings with
--set service.useHostPorts=true
Helm now supports two server-config ownership modes:
serverConfig.mode=managed: renderserverConfig.inlineJsonon every start and mergeuserGroupspasswords from a SecretserverConfig.mode=mutable: require an existing JSON file on the PVC and leave it untouched
Managed mode is the default. The inline config is checked into values, while passwords come from a Secret JSON map keyed by group name. Inline userGroups[].password values are ignored at startup on purpose.
Example password secret:
apiVersion: v1
kind: Secret
metadata:
name: enshrouded-server-passwords
namespace: games
stringData:
user-group-passwords.json: |
{"Default":"replace-me"}Use it with Helm:
helm upgrade --install enshrouded ./helm/enshrouded \
--namespace games --create-namespace \
--set serverConfig.passwordSecret.name=enshrouded-server-passwordsIf you want the UI or manual edits to own the config file instead, switch to mutable mode:
helm upgrade --install enshrouded ./helm/enshrouded \
--namespace games --create-namespace \
--set serverConfig.mode=mutableTo expose the startup save-import UI on a node port:
helm upgrade --install enshrouded ./helm/enshrouded \
--namespace games --create-namespace \
--set service.saveImport.enabled=true \
--set service.saveImport.nodePort=32080 \
--set saveImport.bind=0.0.0.0Install as your current user with rootless systemd --user:
./bare-linux/install.shDetails and installer-only env vars are documented in bare-linux/README.md.
The server config lives at $ENSHROUDED_PATH/enshrouded_server.json.
In env mode, if the file does not exist, the image copies image/enshrouded_server_example.json and applies env overrides on each start.
Runtime config modes:
env(default outside Helm): copy the example config if needed and apply selected env overrides on every startmanaged: render a full config template and merge password values from a secret-backed JSON mapmutable: require an existing config file and do not rewrite it
Common env overrides:
| Env var | Default | Purpose |
|---|---|---|
SERVER_NAME |
Enshrouded Server |
Server browser name |
PORT |
15637 |
Query/game config port |
SERVER_SLOTS |
16 |
Max players |
ENABLE_TEXT_CHAT |
false |
Enable text chat |
SAVE_IMPORT_MODE |
0 |
Startup save-import UI gate |
SAVE_IMPORT_BIND |
127.0.0.1 |
Save-import bind address |
SAVE_IMPORT_PORT |
8080 |
Save-import HTTP port |
EXTERNAL_CONFIG |
0 |
Use your own JSON config without env patching |
ENSHROUDED_CONFIG_MODE |
auto | env, managed, or mutable |
AUTO_UPDATE_ON_BOOT |
1 |
Run Steam update/validate during startup |
When EXTERNAL_CONFIG=1 or ENSHROUDED_CONFIG_MODE=mutable, you must provide your own enshrouded_server.json at ENSHROUDED_CONFIG.
When ENSHROUDED_CONFIG_MODE=managed, set ENSHROUDED_MANAGED_CONFIG_TEMPLATE to a full JSON template and optionally ENSHROUDED_MANAGED_CONFIG_PASSWORDS to a secret-backed JSON map like {"Default":"replace-me"}.
The optional API exposes:
/healthz/v1/stats/
Default port: 8091.
The API sidecar is enabled by default in Helm and the plain StatefulSet. For local usage outside Kubernetes:
python3 ./tools/services/api/live_stats_api.py \
--host 127.0.0.1 \
--game-port 15637 \
--steam-query-port 27015 \
--bind 127.0.0.1 \
--port 8091More examples live in tools/USAGE.md.
Landing page screenshot:
The API always tries to read enshrouded_server.json so it can show effective server metadata such as name, IP, query port, slots, and configured user-group count without extra manual API config.
Debug mode is off by default. To expose the full redacted startup_config block in /v1/stats and in the landing page at /, start the API with --debug or ENSHROUDED_API_DEBUG=1.
When the API needs to read enshrouded_server.json for debug metadata, it resolves the path in this order:
ENSHROUDED_API_SERVER_CONFIG_PATHif set/home/steam/enshrouded/enshrouded_server.json$HOME/enshrouded/enshrouded_server.json$HOME/enshrouded-data/enshrouded/enshrouded_server.json./enshrouded_server.jsonfrom the API process working directory
Probe host resolution order:
--hostorENSHROUDED_API_HOSTif setserver.ipfromenshrouded_server.jsonwhen it is a real address127.0.0.1whenserver.ipis0.0.0.0, empty, missing, or unreadable
Game port resolution order:
--game-portorENSHROUDED_API_GAME_PORTif setserver.queryPortfromenshrouded_server.jsonwhen it is a valid integer15637whenserver.queryPortis missing, invalid, or unreadable
/v1/stats response shape:
{
"ts": "2026-03-09T18:45:00Z",
"host": "Enshrouded Server",
"lane_ports": {
"game_port": 15637,
"steam_query": 27015
},
"live": {
"status": "online",
"source": "query",
"source_lane": "game_port",
"players_current": 3,
"players_max": 16,
"players_confidence": "high",
"server_name": "Enshrouded Server",
"server_version": "0.8.1.0",
"latency_ms": 148.2,
"a2s_player_count": 3,
"a2s_rule_count": 12
},
"lanes": [
{
"lane": "game_port",
"target": "127.0.0.1:15637",
"ok": true,
"status": "online",
"players_current": 3,
"players_max": 16,
"player_count": 3,
"rule_count": 12,
"latency_ms": 148.2,
"error": null
}
],
"local_stats": {
"scope": "api_host",
"cpu_percent": 35.0,
"memory_percent": 42.0,
"memory_used_mb": 860.1,
"memory_total_mb": 2048.0,
"loadavg_1m": 0.22,
"loadavg_5m": 0.18,
"loadavg_15m": 0.15,
"error": null
},
"server_config": {
"config_path": "/home/steam/enshrouded/enshrouded_server.json",
"loaded": true,
"name": "Enshrouded Server",
"ip": "0.0.0.0",
"query_port": 15637,
"slot_count": 16,
"user_group_count": 1,
"error": null
},
"startup_config": {
"server": {
"config_path": "/home/steam/enshrouded/enshrouded_server.json",
"loaded": true,
"config": {},
"error": null
},
"stats_api": {
"bind": "0.0.0.0",
"port": 8091,
"host": "127.0.0.1",
"debug": true,
"timeout": 1.0,
"retries": 2,
"cache_ttl": 3.0,
"lane_ports": {
"game_port": 15637,
"steam_query": 27015
},
"expose_local_stats": false,
"log_events": false,
"webhook": {
"url": null,
"discord_url": null,
"timeout": 3.0,
"events": [
"down",
"high_cpu",
"high_latency",
"high_memory",
"player_add",
"player_remove",
"up"
],
"high_latency_ms": null,
"high_memory_percent": null,
"high_cpu_percent": null
}
}
}
}Notes:
local_statsis omitted unless--expose-local-statsorENSHROUDED_API_EXPOSE_LOCAL_STATS=1is enabled.server_configis included whenever the API can resolve a config path, even when the file is missing or unreadable; in that caseloadedisfalseanderroris populated.startup_configis omitted unless--debugorENSHROUDED_API_DEBUG=1is enabled.- During warmup or probe errors, the API may also include a top-level
errorfield and return placeholder values inlive.
The temporary upload/download UI is disabled by default.
Enable it for a single boot by setting SAVE_IMPORT_MODE=1.
Safer default:
SAVE_IMPORT_BIND=127.0.0.1
Only switch SAVE_IMPORT_BIND to 0.0.0.0 if you intentionally want to expose the UI over the network.
Replay checks:
bash ./tools/tests/replay/test_query_probe.sh
bash ./tools/tests/replay/test_lane_probe_snapshot.sh
bash ./tools/tests/replay/test_live_stats_api.sh
python3 ./tools/tests/replay/test_query_steam_a2s.py ./toolsOptional Helm render check:
helm template enshrouded ./helm/enshrouded >/dev/nullReleases are source tags in this repo. A tag like v0.1.0 publishes:
ghcr.io/shipstuff/enshrouded-server:0.1.0ghcr.io/shipstuff/enshrouded-live-stats-api:0.1.0oci://ghcr.io/shipstuff/charts/enshrouded --version 0.1.0
Prepare and tag a release:
scripts/release.sh 0.1.0 --pushAfter the image and chart workflows pass, production deployments should pin both the chart version and image tags to 0.1.0.
Optional Kustomize render check:
kubectl kustomize . >/dev/null