Skip to main content

Don't Starve Together Dedicated Server

Steam Discord

English | 简体中文

A Rust SDK, Agent and CLI for Don't Starve Together, with Python bindings. Each room runs in one container: one Agent supervises every configured shard and remains available while the games are closed. Published binaries and Python wheels support Linux x86_64 and CPython 3.14. The control socket is for trusted callers on the same host.

Version 0.4.0 replaces the earlier Python implementation. See the migration guide for API changes, verification and known limitations. Internal readiness does not establish public network reachability.

Install the Python SDK with pip install dst-server==0.4.0 in a CPython 3.14 environment. The GitHub release also includes a standalone Linux x86_64 CLI.

Build and Create a Room

Install Rust 1.99, a C/C++ build toolchain, Cap'n Proto, and uv. Host deployment additionally requires Podman with Quadlet and systemd. From this checkout:

cargo build --locked --release -p dst-server --bin dst-server
export PATH="$PWD/target/release:$PATH"
uv sync --python 3.14
dst-server --help
uv run python -m dst_server --help

The native binary and the Python entry point call the same Rust CLI. Build the game image from this revision, supplying the exact game build that SteamCMD will install:

podman build --target game --build-arg GAME_VERSION=756039 \
  -t localhost/dst-server:rust .

The build checks the installed game version and packages the matching SDK Lua bundle. Use the matching published image when creating rooms, or select your locally built image.

The default deployment image is quay.io/wh2099/dst-server:latest. Set deployment.image or --image to quay.io/wh2099/dst-server:beta for beta. Generated Quadlets keep these tags and set AutoUpdate=registry for both published channels. Quadlets use Pull=never; pull the selected image before the first start. Run systemctl enable --now podman-auto-update.timer on the host to enable periodic checks. The timer updates containers and restarts them when a new image is available.

Create a Klei server token and store it in /run/secrets/dst_cluster_token. Run the following with the account that owns the host room directories and systemd services:

import asyncio
from pathlib import Path

from dst_server import Host
from dst_server.settings import Room, build_template


async def main():
    cluster = build_template(
        "pure_survival",
        number=299,
        token=Path("/run/secrets/dst_cluster_token").read_text().strip(),
    )
    room = Room(
        number=299,
        template="pure_survival",
        cluster=cluster,
        deployment={
            "image": "localhost/dst-server:rust",
            "volume_idmap": "uids=0-1000-1;gids=0-1000-1",
        },
    )
    await Host("/srv/dst", "/etc/containers/systemd").create(room)


asyncio.run(main())

This creates /srv/dst/299 and dst-299.container without starting the service. The illustrated ID mapping gives image UID/GID 1000 access to root-owned room files. Choose ownership or mapping for your host. Then start the service:

dst-server host start --room 299
dst-server host status --room 299
dst-server call list_players --socket /srv/dst/299/.dst-agent.sock
journalctl -u dst-299.service -n 100 -f

Use host --root PATH --quadlet-dir PATH for alternate directories, and host --user for the user systemd manager. Room numbers range from 000 to 299 independently of gameplay templates. The packaged fleet contains 000–099 and 200–219:

dst-server template list
dst-server host fleet --room 000,030,209 \
  --token-file /run/secrets/dst_cluster_token --image localhost/dst-server:rust \
  --volume-idmap 'uids=0-1000-1;gids=0-1000-1'

CLI

Command Purpose
host Create, provision, edit, inspect and operate host services; batch by room, template or all rooms.
call Invoke a room or shard operation through its Agent socket.
describe Print method names, scope, argument schemas and default timeouts.
subscribe Read bounded logs, lifecycle or events batches and their discarded counts.
logs, archive Query or follow historical logs; export and upload consistent room archives.
console Execute trusted Lua on an explicit shard, from text, a file or interactive input.
config, template, inspect Validate configuration, inspect schemas and templates, discover native topology.
scripts, annotations, completion Build or verify script bundles, generate Lua annotations and shell completion.
agent Run the room supervisor, normally as the container entry point.

Commands print JSON; --json uses compact JSON and structured errors. A batch preserves individual results and fails its exit status when any target fails. JSON arguments accept inline text, @filename, or - for stdin.

dst-server --json host list
dst-server host show 299 --field /cluster/settings/max_players
dst-server host schema
dst-server host diagnose --room 299
dst-server describe
dst-server call save --socket /srv/dst/299/.dst-agent.sock
dst-server call get_player '{"userid":"KU_example"}' --socket /srv/dst/299/.dst-agent.sock
dst-server subscribe lifecycle --socket /srv/dst/299/.dst-agent.sock

call stop stops games while leaving the Agent available; the opening schedule can start them again. host stop --room 299 stops the container service, including its Agent. Use the latter before offline configuration changes:

dst-server host stop --room 299
dst-server host edit --room 299 --set '/cluster/settings/max_players=9'
dst-server host start --room 299

A closed game can still have a live Agent holding the room lock. Use Agent operations for maintenance while it is running. Changing world-generation settings preserves existing saves; regenerate creates new worlds.

Rust and Python SDKs

The native API exposes typed requests without exposing Cap'n Proto generated types:

use dst_server::{model::{Envelope, Request, Target}, rpc::Client};

async fn inspect_room() -> dst_server::model::Result<serde_json::Value> {
    let client = Client::connect("/srv/dst/299/.dst-agent.sock").await?;
    let result = client.call(Envelope::new(Target::Room, Request::Status {})?).await;
    client.close().await?;
    result
}

Python exposes asynchronous calls and explicit connection and subscription cleanup:

import asyncio
from dst_server import Client


async def main():
    async with await Client.connect("/srv/dst/299/.dst-agent.sock") as client:
        print(await client.call("status"))
        print(await client.call("save"))
        async with await client.subscribe("events") as events:
            print(await events.next(max_items=100))


asyncio.run(main())

Use describe() or dst-server describe --shard for the exact method contract. Operations cover world queries, saves, rollback, regeneration, pause and player control. They also cover migration, permissions, Mods and exports. Player operations locate userid across shards and verify its identity before execution. Pass shard="forest" when an operation needs an explicit shard. Entity references contain world session and runtime generation; old references become invalid after reload.

Each room permits one mutation at a time and returns busy for a conflicting operation. Accepted mutations belong to the Agent. Cancellation, disconnection and caller timeouts stop waiting without undoing the operation. Check status after an unknown result before deciding whether to retry. Save success requires native completion on every shard. Normal shutdown requires exit code zero, process reaping and output drainage, with no force or protocol fault. Failed shutdown retains each shard's exit status and confirmed saved snapshot. Signal termination uses the negative signal number. The default stop wait is 220 seconds, including its notice; changing the notice delay adjusts that budget. Mods that disable saving can stop cleanly with saved_snapshot: null. An explicit save returns an error when no new snapshot is confirmed. Migration success requires confirmation in the destination world. Console success means the Lua code returned; an asynchronous effect started by that code may still be running.

Configuration and Files

/srv/dst/299/
├── cluster.ini
├── cluster_token.txt
├── adminlist.txt / blocklist.txt / whitelist.txt
├── .dst-control.json
├── .dst-room.lock
├── .dst-agent.sock
├── mods/
│   ├── dedicated_server_mods_setup.lua
│   ├── modsettings.lua
│   └── ugc/
├── forest/
│   ├── server.ini
│   ├── worldgenoverride.lua
│   ├── modoverrides.lua
│   └── save/
└── cave/
    └── server.ini, world overrides, Mods and saves

Native INI/Lua files define game configuration; the Quadlet defines deployment. .dst-control.json stores the template label, policy, activity and recovery state. It is not a second copy of game settings. Keep it across container replacement to retain recovery limits and maintenance progress. Never remove or replace .dst-room.lock while a room exists.

Every directory with server.ini is an enabled shard. Names are arbitrary, with exactly one master. Keys and IDs must be consistent, with conflict-free game, query and shard ports. Port allocation uses the actual shard set, an allocation lock and a persistent map; the previous four-shard limit is removed. The default host allocation pool is 30000–65535. All shards share the container network and room Mods; each process has separate control pipes and readers.

Python configuration factories validate through Rust:

from dst_server.settings import ClusterSettings, RoomStore, build_template

settings = ClusterSettings(max_players=9, cluster_name="Friday games")
cluster = build_template("pure_endless", number=299, settings=settings)
print(cluster.dump(defaults=True))
print(settings.schema())

store = RoomStore("/srv/dst")
room = store.load(299)
updated = room.edit("/cluster/settings/cluster_description", "Friday games")
# After stopping the entire service:
# store.save(updated)

dump() redacts token, shared key and password; secrets=True explicitly returns them. Rendered native files and files() contain real credentials and must be handled accordingly. Defaults are applied on demand; omitted fields and explicit None remain distinct. Frozen schemas, defaults and presets are packaged with Rust, including 12 templates and all 120 fleet rooms. Rust owns validation and file operations; Python provides bindings, conversion and asynchronous wrappers.

Offline writes use the shared room lock, reject symlinks and commit recoverable file transactions with synchronization. General configuration saves preserve live admin, ban and whitelist files. Use permission operations for online changes. Structured Lua reads parse Lua 5.1 literals without execution. Dynamic configurations can be loaded by the game; this API cannot structurally edit them.

Opening Hours, Maintenance and Recovery

The default policy uses Asia/Shanghai, opens all day, enables automatic Mod maintenance and disables idle world regeneration. A policy can be sent to a running Agent:

dst-server call set_policy - --socket /srv/dst/299/.dst-agent.sock <<'JSON'
{
  "policy": {
    "timezone": "Asia/Shanghai",
    "schedule": [{"start": "18:00", "end": "00:00"}],
    "mod_auto_update": true,
    "idle_regeneration": false
  }
}
JSON

The Agent checks schedules every 30 seconds, including windows across midnight. At closing time it stops all games and stays available for the next opening. Pending Mod maintenance runs before reopening. The default maintenance countdown is 60 seconds with announcements every 30 seconds. Failed downloads or validation receive two additional attempts, five minutes apart, then leave games closed. An explicit maintenance request or a later opening can retry.

A required shard crash or sustained control failure stops the whole room. Abnormal process exits, forced termination and IPC faults require container replacement. Recovery state persists across that replacement. Retryable failures allow two restarts of the current save, 30 seconds apart. Only confirmed save-loading failure can then try one previous complete room snapshot. The loss must be proven to be at most one game day per world. Targets are fixed before mutation; interrupted recovery resumes those same targets. Unknown state, missing targets, unsupported failure causes or failed recovery leave the room closed. Thirty minutes of stable operation restores the budget. Exhausted rooms get another opportunity at the next opening, or explicit start for an always-open room.

Idle regeneration runs only when enabled, during opening hours, with every shard ready and reliably empty. Its retention periods are 6, 24, 36, 72 and 168 hours according to world age. Interrupted observations or changed worlds restart the retention period; clean scheduled closure preserves it.

Logs, Exports and Utilities

The default game event profile is history; agent --profile also accepts critical and off. Events are bounded and best effort. Source gaps, subscriber overflow, output loss and export rejection have separate counters. A subscription is not a replay log or a durable task trigger.

OTLP uses gRPC for logs, metrics and traces. Signal-specific settings override common OTEL_EXPORTER_OTLP_* settings. Their prefixes are OTEL_EXPORTER_OTLP_LOGS_*, OTEL_EXPORTER_OTLP_METRICS_* and OTEL_EXPORTER_OTLP_TRACES_*. Configure endpoint, headers, TLS certificates, compression and timeout explicitly; timeout values are milliseconds. Set a signal's OTEL_*_EXPORTER=none, or OTEL_SDK_DISABLED=true for all signals, to disable export. Structured log transport preserves null, integers, floats, arrays and objects. Netdata's flattened query projection has separate type limitations.

dst_server.logs provides JournalLogs, JournalQuery, JournalStream, NetdataLogs and NetdataLogQuery. The CLI accepts those same query fields, including cursors, direction and filters:

dst-server logs journal --unit dst-299.service --query '{"limit":100,"since":"today"}'
dst-server logs journal --unit dst-299.service --follow
dst-server logs telemetry '{"since":1790899200,"until":1790985600,"limit":100}'

A follow stream emits one record at a time; interrupting a log query waits for its reader process to exit. dst_server.events exposes the native event schema and validation through event_schema() and validate_event(). dst_server.KleiClient provides bounded build, region, lobby and room requests with per-query batch failures. HTTP failures are distinct from successful empty results.

Archive export prepares all shards while games are stopped and preserves native world/player data and Mod progress. It removes credentials, permission lists and SDK runtime files. Compression uses 7z with Zstd level 3 by default and one concurrent compressor. Uploads use the native object-store implementation and explicitly abort incomplete multipart uploads on failure or cancellation. Cleanup failures remain visible in the result and logs. Python exposes dst_server.archive.export_cluster and ClusterArchive.save/upload. Export requires stopped games and uses the live Agent or an exclusive offline lock. For an offline local export:

dst-server host stop --room 299
dst-server archive export 299 --output /tmp/room-299.7z

The output path must not already exist. See the migration guide for entry points and evidence.

dst-server scripts build /path/to/native/scripts.zip --output /tmp/scripts.zip
dst-server scripts verify /tmp/scripts.zip --source /path/to/native/scripts.zip
dst-server annotations components /path/to/scripts/components --output /tmp/components.lua
dst-server annotations modutil /path/to/scripts/modutil.lua --output /tmp/modutil.lua

Development and Validation

The workspace contains crates/dst-server and crates/dst-server-python. Python wrappers live under python/dst_server, with Lua resources under resources/lua. The container runs the Rust binary directly.

cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets -- -D warnings
cargo test --locked -p dst-server --all-targets
uv run ruff check python tests/rust tools
uv run ty check python
uv run python tests/rust/check_lua.py
uv build --python 3.14 --out-dir dist --no-sources

CI builds a wheel from the source distribution and installs it in an isolated environment. It then runs native binding and process tests. Lua contracts run under Lua 5.1 and LuaJIT. Game build 756039 has an unresolved native shutdown failure observed with five connected shards. Gorge and Forge deliberately disable saving; the SDK reports an unconfirmed save and can still close them cleanly. Real game probes use disposable directories and containers; they must not target live rooms. Build manifests record SDK, Lua, protocol, native game and artifact identities. See the migration guide for verification and the deployment procedure.

Metadata

Release files for dst-server 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dst-server 0.4.0
File Size Uploaded
dst_server-0.4.0.tar.gz 450.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dst-server 0.4.0
File Interpreter ABI Platform
dst_server-0.4.0-cp314-cp314-manylinux_2_39_x86_64.whl CPython 3.14 CPython 3.14 Linux glibc 2.39+ x86-64 Details

Total release size: 14.9 MB

Release files / dst_server-0.4.0.tar.gz

Download URL dst_server-0.4.0.tar.gz
Size 450.9 kB
Tags Source
SHA-256 checksum
How to use checksums
05258d97f865aedf74de2377d99e40b04658ecbda15ce56430b9fda359071be0
BLAKE2b-256 checksum
How to use checksums
f400a7a4293ede10fcaa1711781a0640d7d57898a5f8dc2bc99898d77975a3d7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release files / dst_server-0.4.0-cp314-cp314-manylinux_2_39_x86_64.whl

Download URL dst_server-0.4.0-cp314-cp314-manylinux_2_39_x86_64.whl
Size 14.4 MB
Tags CPython 3.14 Linux glibc 2.39+ x86-64
SHA-256 checksum
How to use checksums
1d57b562c3ddd778d45e45d5062906ac4efb80ef18257b8ea48edac39600b70d
BLAKE2b-256 checksum
How to use checksums
1a1ed38314139f11f48860920b18ee6b959a92d564d3da11e7b571fba7d04dbe
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 3, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

2 release files

This release

0.4.0 This release

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page