← LeaderboardThe Skill Leaderboard

dedicated-server

Use when building dedicated servers — headless export, server architecture, lobby management, and deployment

Published by jame581 ↗jame581/GodotPrompter

Try your first task

dedicated-server

Start a fresh conversation. Replace the highlighted placeholders.

Use dedicated-server to implement a headless authoritative dedicated-server architecture for [Godot project path] in [GDScript or C#], including server/client separation, lobby and match flow, and configurable server startup; do not deploy it without my explicit approval.

Required inputs

  • [Godot project path]
  • [GDScript or C#]

User-approved starter prompt based on a pinned source version; check current skill requirements before running. Task evidence ↗

SKILL.md

name: dedicated-serverdescription: Use when building dedicated servers — headless export, server architecture, lobby management, and deployment

Dedicated 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:

  1. Open Project → Export.
  2. Add a Linux preset and name it Linux Server.
  3. On the Resources tab, set Export Mode to Export as dedicated server. This adds the dedicated_server feature tag — which makes the binary start with the headless display and dummy audio drivers even without --headless — and marks res:// 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.
  4. 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:

TagSet byNotes
dedicated_serverDedicated-server exportMost reliable way to detect a server binary
(no tag) --headlessCLI flagNot a feature tag — OS.has_feature("headless") is false; check DisplayServer.get_name() == "headless"
Custom serverYour export preset's Custom FeaturesUseful 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_server feature 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") or DisplayServer.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_DISABLED on clients; client-only nodes use PROCESS_MODE_DISABLED on the server
  • Engine.is_editor_hint() guard at the top of _ready() in every script that has side effects
  • ServerConfig parses --port, --max-players, --tick-rate from CLI args before _ready() of other autoloads
  • Config file loading falls back gracefully when server.cfg does not exist
  • Environment variables (SERVER_PORT, SERVER_MAX_PLAYERS, SERVER_TICK_RATE) are applied after config file, before CLI args
  • LobbyManager.player_list size is checked against max_players before accepting a new peer
  • Peer is kicked via RPC before closing if the lobby is full
  • Ready states are reset when MatchState returns to LOBBY so players must re-confirm each round
  • MatchManager only runs _physics_process on the server (SetPhysicsProcess(false) on clients)
  • Countdown and results timers are driven by _physics_process delta, not Timer nodes (avoids scene dependency)
  • Dockerfile copies the binary and the .pck file (plus the data_<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-failure so 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_server feature tag added
  • RenderingServer.set_render_loop_enabled(false) (and audio bus muted) when OS.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