Building Your Own Server Image

The supervisor contract every bring-your-own image has to meet, the Dockerfile for Linux and Windows, and how to test the image before you point a branch at it.

A custom game server image on PingCore has to contain the PingCore supervisor. The platform pins the main container's command to the supervisor and ignores whatever ENTRYPOINT or CMD the image declares:

  • Linux: /usr/bin/tini -- node /node-service/src/index.js

  • Windows: C:\Program Files\nodejs\node.exe C:\node-service\src\index.js

If those paths hold nothing, the container has no process to run, so it fails to start and the deployment job records the failure. Agones and GameLift require their SDK inside your build in the same way. There is no sidecar mode and no "run any container" mode.

What the supervisor provides

Everything the panel and the platform do inside a running server goes through the supervisor:

  • the live console and console commands

  • restart, stop and start

  • content sync from the CDN, from Steam, or from the image

  • the memory governor, which restarts a leaking server before it affects the node

  • scheduled tasks

  • log and output capture

  • SFTP access to the server's files

  • the Discovery fleet agent

An image without the supervisor has none of this.

Start from the supervisor base image

Build FROM repo.pingcore.io/pingcore/supervisor-base:<tag> and copy your game into /opt/game (C:\game on Windows). The base already holds the supervisor, its runtime, its tools and the directory layout, so there is nothing to install to satisfy the contract. Supervisor Base Images lists the tags.

If your game needs a base the PingCore images cannot provide, see Advanced: using your own base image.

The Dockerfile

ARG the build id so one line stamps both the label and the .version marker.

Linux

FROM repo.pingcore.io/pingcore/supervisor-base:ubuntu-24.04-1.1.0

ARG GAME_BUILD=dev

RUN apt-get update && apt-get install -y --no-install-recommends \
      libsdl2-2.0-0 \
    && rm -rf /var/lib/apt/lists/*

COPY ./build /opt/game
RUN echo "${GAME_BUILD}" > /opt/game/.version

Windows

FROM repo.pingcore.io/pingcore/supervisor-base:windows-ltsc2025-1.1.0

ARG GAME_BUILD=dev

COPY ./build C:/game
RUN Set-Content -Path 'C:\game\.version' -Value $env:GAME_BUILD

Build it with --build-arg GAME_BUILD=2026.09.15-abc123.

Image requirements

  • Put the game at /opt/game or C:\game. The supervisor copies that tree onto the server's data volume at install time.

  • Ship a .version file at the root of that tree, holding one line with an opaque build id. Only the first non-empty line is read. Without it the content is treated as unversioned and only installs on first boot and on reinstall.

  • GAME_IMAGE_ROOT and GAME_IMAGE_VERSION are yours to set with ENV if your build lands somewhere else. GAME_IMAGE_ROOT must be an absolute path outside the data volume. The supervisor refuses a relative path, a filesystem root, the gameserver directory, any path under the data volume, and any path that contains the data volume.

  • Do not set USER. The supervisor runs as root, chowns the data volume, creates the gameserver user when the image lacks it, and spawns the game under that user itself. A non-root USER breaks the chown and leaves the game unable to write its own files.

  • Do not rely on ENTRYPOINT or CMD. Both are ignored on the main container. Put startup work in a container_start or process_start script on the template set instead.

  • Never bake BOOTSTRAP_FILE. The platform emits it as an empty value on every container, which is the instruction to fetch the real bootstrap from the API. An image-supplied bootstrap file would hand the supervisor a configuration the image author controls.

  • Write runtime data under the data volume. The main container carries an ephemeral-storage limit: a 4Gi baseline plus the size limits of the disk-backed ephemeral volumes mounted by any container in the pod. A process that fills the container's own writable layer gets the pod evicted.

  • Do not chown or chmod the game tree after COPY in a Linux Dockerfile. Overlay copy-up duplicates the whole tree into a new layer, doubling the image size. The supervisor sets ownership on the copy it makes, never on the image root.

Environment variable names the platform owns

The platform emits these on every container, after your rows, so a value baked into your image is overridden. Do not set them:

SERVER_ID, API_KEY, API_URL, BOOTSTRAP_FILE, CORS_ORIGINS, STEAMCMD_PATH, DEPOTDOWNLOADER_PATH, POD_IP, AGONES_SDK_HTTP_PORT, UPDATE_POLL_INTERVAL, DEPOT_UPDATE_POLL_INTERVAL, DEPOT_MAX_DOWNLOADS, RESOURCE_SAMPLE_INTERVAL, SYSTEM_DISABLE_MEMORY_MONITORING, ALERT_RETENTION_MS, TASK_CHECK_INTERVAL, TASK_MIN_UPTIME_SECONDS, ENVIRONMENT.

Two whole prefixes are reserved as well: PVC_ (mount paths the platform derives per volume, including PVC_DATA) and MEMORY_ (the memory governor's thresholds).

GAME_IMAGE_ROOT and GAME_IMAGE_VERSION are deliberately not reserved, because they describe your image rather than the platform.

Test the image locally

Write a minimal bootstrap next to your Dockerfile. It stands in for the payload the platform normally serves.

{
  "gameId": 1,
  "gameServerId": 1,
  "features": { "autoBoot": true, "autoUpdate": false, "updateOnBoot": false },
  "imageContent": { "enabled": true },
  "configs": {
    "cli": [{ "command": "./MyGameServer -port 7777" }],
    "files": [],
    "scripts": []
  },
  "apiService": { "enabled": true, "port": 8080 },
  "healthCheck": { "gamePorts": [7777], "initialDelaySeconds": 10 }
}

Run the image with the four variables the supervisor needs. BOOTSTRAP_FILE has to be an absolute path outside PVC_DATA, so keep it out of the data volume:

docker run --rm -it \
  -e SERVER_ID=1 \
  -e API_KEY=local-only \
  -e PVC_DATA=/gameserver \
  -e BOOTSTRAP_FILE=/etc/pingcore/bootstrap.json \
  -v "$PWD/bootstrap.json:/etc/pingcore/bootstrap.json:ro" \
  -p 8080:8080 \
  yourstudio/your-game:dev

Then check the supervisor is up:

curl http://localhost:8080/health

It answers {"status":"ok"} without authentication. That is the same check the platform runs against every server after a deployment, so an image that fails it locally fails the deploy too. The panel's live console and file browser run over Socket.IO against the platform, so they are not part of the local check.

Point a branch at the image

  1. Add the image under Workspace > Games > Container Images, with the Registry, Repository and Tag of your build. See Pushing Images to the Registry.

  2. Select it on the game's main container, under Workspace > Games, open the game, Containers.

  3. Go to Branches, edit the branch, and in Data Configuration set Data Source to Baked into container image.

There is nothing else to configure on the branch. Baked-In Game Files covers when the copy runs and how updates reach existing servers.