About dedicated-server
A Godot 4.3+ skill for designing and deploying dedicated multiplayer game servers. It covers headless exports, server-side game flow, configuration, and Linux deployment with Docker and systemd.
Key features
- 01Headless server exports
- 02Server and client logic separation
- 03Lobby and match state patterns
- 04CLI-based server configuration
- 05Docker and systemd deployment
Use cases
- 01Prepare a headless multiplayer server build
- 02Separate server systems from client-only scenes
- 03Implement authoritative lobby and match flow
- 04Deploy a game server on a Linux VPS
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ jame581/ godotprompter --skill dedicated-serverAfter installing, type/dedicated-serverin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/deployment.mdlobby-management.mdmatch-flow.mdserver-config.mdDetails
From the project READMEDedicated Server in Godot 4.3+
All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, C# follows.
Related skills: See multiplayer-basics for ENet setup, RPCs, and authority model. See multiplayer-sync for state synchronization and interpolation.
1. Headless Export
A dedicated server runs without a display, GPU, or audio device. Godot supports this through the --headless flag and a dedicated export preset.
--headless Flag
Pass --headless on the command line to suppress the display and audio drivers at runtime:
./my_game.x86_64 --headless
--headless is a runtime flag that works on any exported binary. Godot 4 has no separate server platform or server export template: a dedicated-server export uses the regular template, so the binary keeps its rendering code (a smaller binary needs a custom-compiled template). What the export changes is the PCK and a feature tag — see below.
Server Export Preset
In the Godot editor, create a dedicated Linux export preset:
- Open Project → Export.
- Add a Linux preset and name it
Linux Server. - On the Resources tab, set Export Mode to Export as dedicated server. This adds the
dedicated_serverfeature tag — which makes the binary start with the headless display and dummy audio drivers even without--headless— and marksres://as Strip Visuals: visual resources are exported as placeholders (e.g. a texture keeps only its size). Set folders the server needs intact to Keep. - Use the Exclude filter on the same tab to drop client-only files (audio, client-only scenes) from the server PCK entirely.
Feature Tags
Use OS.has_feature() to branch between server and client code at runtime. Define a custom server feature on the export preset's Features tab (Custom (comma-separated) field) or rely on the built-in dedicated_server feature that a dedicated-server export sets automatically:
# boot.gd — autoload, runs before any scene loads
extends Node
func _ready() -> void:
if OS.has_feature("dedicated_server") or DisplayServer.get_name() == "headless":
# Disable rendering-dependent systems
RenderingServer.set_render_loop_enabled(false)
# Start server logic
ServerBootstrap.start()
else:
# Start client logic
ClientBootstrap.start()
// Boot.cs — autoload, runs before any scene loads.
using Godot;
public partial class Boot : Node
{
public override void _Ready()
{
if (OS.HasFeature("dedicated_server") || DisplayServer.GetName() == "headless")
{
// Disable the render loop. The window is invisible but the engine still ticks.
RenderingServer.SetRenderLoopEnabled(false);
ServerBootstrap.Start();
}
else
{
ClientBootstrap.Start();
}
}
}
Note: the export preset configuration (custom features, exclude filter, "Export as dedicated server" mode) is identical regardless of language — see the GDScript section above for preset settings.
Feature tag summary:
| Tag | Set by | Notes |
|---|---|---|
dedicated_server | Dedicated-server export | Most reliable way to detect a server binary |
(no tag) --headless | CLI flag | Not a feature tag — OS.has_feature("headless") is false; check DisplayServer.get_name() == "headless" |
Custom server | Your export preset's Custom Features | Useful when sharing a binary between roles |
2. Server Architecture
Game Loop Without Rendering
On a headless server, _process and _physics_process still run normally — but nothing is rendered. Keep all server logic in _physics_process for deterministic, fixed-rate updates.
GDScript
# server_main.gd — add as autoload named ServerMain
extends Node
## Physics frames per second — matches Project Settings → Physics → Common → Physics Ticks Per Second.
## Override via --tick-rate CLI argument (see Section 5).
var tick_rate: int = 60
## Current server tick counter.
var server_tick: int = 0
func _ready() -> void:
# Guard: this node does nothing on the client.
if not _is_server():
set_process(false)
set_physics_process(false)
return
Engine.physics_ticks_per_second = tick_rate
print("[Server] Started — tick rate: %d Hz" % tick_rate)
func _physics_process(_delta: float) -> void:
server_tick += 1
_tick_game_logic()
func _tick_game_logic() -> void:
# All authoritative game simulation goes here.
# Never reference Camera, CanvasLayer, or any rendering node from this path.
pass
## Returns true when this process is acting as the authoritative server.
func _is_server() -> bool:
# Covers both: dedicated binary and hosted listen-server.
return multiplayer.is_server()
Server-Only Logic Separated from Client
Structure your scenes so server-only nodes are in a dedicated branch and skipped on clients:
# world.gd
extends Node
@onready var server_systems: Node = $ServerSystems # physics, AI, scoring
@onready var client_systems: Node = $ClientSystems # camera, HUD, audio
func _ready() -> void:
# Disable server systems on clients and vice versa.
server_systems.set_process_mode(
PROCESS_MODE_ALWAYS if multiplayer.is_server() else PROCESS_MODE_DISABLED
)
client_systems.set_process_mode(
PROCESS_MODE_DISABLED if multiplayer.is_server() else PROCESS_MODE_ALWAYS
)
Engine.is_editor_hint() + is_server Checks
Use these guards at the top of scripts that must behave differently in the editor, on the server, and on clients:
func _ready() -> void:
if Engine.is_editor_hint():
return # Skip all runtime setup in editor preview
if multiplayer.is_server():
_server_init()
else:
_client_init()
func _server_init() -> void:
print("[Server] Initializing authoritative state")
func _client_init() -> void:
print("[Client] Initializing local presentation layer")
C#
// ServerMain.cs — add as autoload named ServerMain
using Godot;
public partial class ServerMain : Node
{
/// <summary>Physics ticks per second. Override via --tick-rate CLI argument.</summary>
public int TickRate { get; set; } = 60;
/// <summary>Current server tick counter.</summary>
public int ServerTick { get; private set; }
public override void _Ready()
{
if (!IsServer())
{
SetProcess(false);
SetPhysicsProcess(false);
return;
}
Engine.PhysicsTicksPerSecond = TickRate;
GD.Print($"[Server] Started — tick rate: {TickRate} Hz");
}
public override void _PhysicsProcess(double delta)
{
ServerTick++;
TickGameLogic();
}
private void TickGameLogic()
{
// All authoritative game simulation goes here.
}
private bool IsServer() => Multiplayer.IsServer();
}
// World.cs
using Godot;
public partial class World : Node
{
[Export] private Node _serverSystems = null!;
[Export] private Node _clientSystems = null!;
public override void _Ready()
{
if (Engine.IsEditorHint()) return;
_serverSystems.ProcessMode = Multiplayer.IsServer()
? ProcessModeEnum.Always
: ProcessModeEnum.Disabled;
_clientSystems.ProcessMode = Multiplayer.IsServer()
? ProcessModeEnum.Disabled
: ProcessModeEnum.Always;
}
}
7. Checklist
- Export preset is exported as a dedicated server (
dedicated_serverfeature is set automatically) - Client-only assets (shaders, audio, high-res textures) are excluded from the server PCK
- Boot script checks
OS.has_feature("dedicated_server")orDisplayServer.get_name() == "headless"to branch server vs client startup -
RenderingServer.set_render_loop_enabled(false)called on the server to prevent any render work - Server-only nodes use
PROCESS_MODE_DISABLEDon clients; client-only nodes usePROCESS_MODE_DISABLEDon the server -
Engine.is_editor_hint()guard at the top of_ready()in every script that has side effects -
ServerConfigparses--port,--max-players,--tick-ratefrom CLI args before_ready()of other autoloads - Config file loading falls back gracefully when
server.cfgdoes not exist - Environment variables (
SERVER_PORT,SERVER_MAX_PLAYERS,SERVER_TICK_RATE) are applied after config file, before CLI args -
LobbyManager.player_listsize is checked againstmax_playersbefore accepting a new peer - Peer is kicked via RPC before closing if the lobby is full
- Ready states are reset when
MatchStatereturns toLOBBYso players must re-confirm each round -
MatchManageronly runs_physics_processon the server (SetPhysicsProcess(false)on clients) - Countdown and results timers are driven by
_physics_processdelta, notTimernodes (avoids scene dependency) - Dockerfile copies the binary and the
.pckfile (plus thedata_<project>_<platform>_<arch>folder for C#) to the image - UDP port is opened in the VPS firewall before the first test run
- systemd service has
Restart=on-failureso the server recovers from crashes automatically - Logs are routed to the systemd journal and inspectable with
journalctl -u <service> -f
3. Lobby System
A per-player state dictionary keyed by peer_id is the canonical pattern. Server holds the authoritative dict; clients receive updates via RPC. Implement a --max-players CLI cap and a ready-toggle RPC so all peers can confirm before starting.
See references/lobby-management.md for the full GDScript and C# lobby implementation (player_list dict, max-players cap, ready toggle RPC, broadcast pattern).
4. Match Flow
Drive lobby → countdown → in-game → results with a state machine. Server is authoritative — clients only receive state-change RPCs. Common states: LOBBY, COUNTDOWN, IN_GAME, RESULTS.
See references/match-flow.md for the full state machine with countdown/results timers and GDScript + C# implementations.
5. Server Configuration
Parse CLI flags from OS.get_cmdline_args() for --port, --max-players, --tick-rate, --log-level. Pre-set Engine.physics_ticks_per_second before the first physics frame; reading and writing the others is straightforward match / switch work.
See references/server-config.md for the GDScript and C# argument-parsing helper that reads all four flags safely at startup.
6. Deployment
A Linux VPS with a Dockerfile and systemd service file is the standard production layout. The Dockerfile bundles the exported binary, its PCK, and for C# projects the data_* folder that holds the assemblies and the bundled .NET runtime. systemd handles auto-restart, log rotation via journald, and resource limits.
See references/deployment.md for the Dockerfile, the Linux VPS setup steps, the systemd unit file, and log-rotation configuration.
7. Checklist
- Headless export preset created and
dedicated_serverfeature tag added -
RenderingServer.set_render_loop_enabled(false)(and audio bus muted) whenOS.has_feature("dedicated_server")is true - Server-side autoloads check
multiplayer.is_server()before initializing systems - CLI args parsed at startup for port, max-players, tick-rate, log-level
- Lobby state lives only on the server; clients receive updates via RPC
- Match-flow state machine has explicit transitions and a single source of truth (server)
- Production deployment uses systemd or Docker for auto-restart and log rotation
- Headless build is exported with Export with Debug off in production presets