- Shell 61.5%
- Dockerfile 38.5%
| scripts | ||
| .dockerignore | ||
| .gitignore | ||
| .justfile | ||
| .woodpecker.yml | ||
| compose.yml | ||
| Dockerfile | ||
| README.md | ||
| renovate.json | ||
frost
Containerized dedicated game servers for the homelab, built on
steamcmd.
A single multi-stage Dockerfile produces one image per game. Each stage
installs the game's runtime dependencies, ships a small entrypoint script that
pulls the latest build from Steam on container start, and then execs the server
binary. Images are published to oci.bvoy.dev/homelab/frost by Woodpecker CI.
| Target | Steam App ID | Ports | Status |
|---|---|---|---|
valheim |
896660 | 2456-2457/udp | Working |
astroneer |
728470 | 8777/udp | WIP — the entrypoint installs the app but does not yet launch the server |
Layout
Dockerfile multi-stage build; one stage (target) per game
compose.yml local/homelab deployment
scripts/*.sh per-game entrypoints (steamcmd update + server launch)
.justfile helper recipes for poking at containers
.woodpecker.yml CI: matrix build of every target via kaniko
.env deployment config (gitignored)
Inside the container everything lives under SERVER_ROOT=/var/lib/frost:
/var/lib/frost/servers/<game>— game files installed by steamcmd/var/lib/frost/saves— world/save data
Both are bind-mounted from the host so that game updates and worlds survive a container rebuild.
Configuration
.env is not committed. Create one alongside compose.yml:
STORAGE_SAVES=/mnt/tardis/pool/data/frost/steam
STORAGE_SERVERS=/mnt/tardis/pool/data/frost/servers
# valheim
SERVER_NAME=OGABOGA
WORLD_NAME=OGABOGA
WORLD_PASSWORD=CHANGEME
# published image tag; pin to <target>-<commit-sha> to roll back
FROST_TAG=valheim
| Variable | Default | Purpose |
|---|---|---|
STORAGE_SAVES |
— | Host path bind-mounted to /var/lib/frost/saves |
STORAGE_SERVERS |
— | Host path bind-mounted to /var/lib/frost/servers |
STEAM_USER |
anonymous |
Steam account used by steamcmd |
SERVER_NAME |
OGABOGA |
Server name shown in the browser |
WORLD_NAME |
OGABOGA |
Valheim world to load or create |
WORLD_PASSWORD |
— | Valheim join password (min. 5 characters) |
FROST_TAG |
valheim |
Image tag to pull |
Running
Pull and run the published image:
docker compose up -d valheim
docker compose logs -f valheim
Build locally instead of pulling:
docker compose build valheim
docker compose up -d valheim
The entrypoint runs steamcmd +app_update … validate on every start, so the
first boot takes a while and each restart picks up the latest game build.
just recipes
just # list recipes
just start <service> # run ./start.sh in a throwaway container
just exec <service> [cmd] # shell into a running container (default: sh)
just run <service> [cmd] # one-off container with sh as the entrypoint
just login [user] [pass] # run steamcmd +login, e.g. to handle Steam Guard
The recipes default to a steamcmd service; pass a service defined in
compose.yml (e.g. just exec valheim) for anything real.
CI
.woodpecker.yml builds every target in the TARGET matrix with kaniko on
pushes to main (and on manual/deployment events), pushing two tags per target:
oci.bvoy.dev/homelab/frost:<target>— moving tagoci.bvoy.dev/homelab/frost:<target>-<commit-sha>— immutable, use it inFROST_TAGto roll back
Registry credentials come from the DOCKER_AUTH_CONFIG secret. Renovate keeps
base images and CI images up to date.
Adding a game
- Add a stage to the
Dockerfileoffbase, installing whatever runtime libs the server needs andEXPOSE-ing its ports. - Drop an entrypoint in
scripts/<game>.shthat runssteamcmd +app_update <appid> validateand thenexecs the server binary. - Add the stage name to the
TARGETmatrix in.woodpecker.yml. - Add a service to
compose.ymlpointing at the new target.