Godot 4 Dedicated Server Setup: Headless Export, ENet & Backend

Build a Godot 4 dedicated server end to end: dedicated-server export, headless Linux, ENet, player data, server registry, readiness, and secure admission.

A local Godot multiplayer session proves the game loop; production adds a different set of problems. The authoritative process has to run without a renderer, clients need a stable way to find and authenticate to it, progression must survive process restarts, and dead sessions must be detected and removed.

This guide follows that production path end to end: separate client/server logic, export a headless Linux build, run ENet from an authoritative process, add authentication and persistence, then manage server readiness and lifecycle.

Scope: this guide focuses on an authoritative dedicated process rather than a listen server or peer-to-peer host. Moving authority off the player client reduces what a modified client can decide, but it does not make a game cheat-proof by itself; validation still belongs in your server-side game logic.

1. The Godot 4 Multiplayer Architecture Overview

Before diving into the code and backend infrastructure, it is crucial to understand how Godot 4 handles multiplayer conceptually. Godot uses a high-level multiplayer API built on top of the ENetConnection class by default. The architecture typically consists of three distinct layers when deployed to production:

  • The Game Client: The Godot executable running on the player's device. It handles rendering, input gathering, and interpolating state received from the server.
  • The Dedicated Server (Headless Godot): A Godot executable running on a Linux virtual machine or container in the cloud without a graphical interface. It runs the authoritative game logic, physics, and validates player inputs.
  • The Platform Backend (API/Database): An external web service (such as Crux, PlayFab, or Nakama) that the dedicated server communicates with via HTTP. This layer handles player accounts, inventory, leaderboards, and matchmaking.

Keep durable progression outside a short-lived match process. A session server can keep hot simulation state in memory, while player progression and other data that must survive a restart should be written to durable storage. Persistent-world games are a different shape: they often checkpoint world state as well as player state, so “stateless server” is not a universal rule.

2. Preparing Godot for Dedicated Servers

To run a dedicated server, you must structure your Godot project so that it can easily switch between "Client Mode" and "Server Mode". While you can maintain two separate Godot projects, it is much more efficient to use a single project with split logic.

Separating Client and Server Logic

In Godot 4, you can use the OS.has_feature("dedicated_server") method to branch your code based on the export template. Furthermore, the multiplayer.is_server() method allows you to execute code only on the server.

func _ready():
    if OS.has_feature("dedicated_server"):
        print("Starting in Server Mode...")
        start_server()
    else:
        print("Starting in Client Mode...")
        show_main_menu()

func start_server():
    var peer = ENetMultiplayerPeer.new()
    var err = peer.create_server(PORT, MAX_CLIENTS)
    if err == OK:
        multiplayer.multiplayer_peer = peer
        print("Server listening on port ", PORT)
    else:
        print("Failed to start server: ", err)

Exporting a Headless Linux Build

Godot 4 no longer needs a special server binary. Create a Linux export preset for the server and use the preset's Resources → Export as dedicated server mode. Godot strips visual resources where possible, automatically adds the dedicated_server feature tag, and starts that export headlessly. You can also run a normal Godot binary with --headless when that better fits development or CI.

  1. Open Project > Export and create a Linux preset for the server build.
  2. Under Resources, choose Export as dedicated server.
  3. Use OS.has_feature("dedicated_server") to enter server mode for that export.
  4. Only keep visual resources the server actually reads; removing a referenced resource can make a scene fail to load.

Godot documents both the dedicated-server export mode and the --headless route in its dedicated-server export guide.

3. Add Auth and Persistent Data Without Inventing an Endpoint

Networking identity and backend identity are separate concerns. Godot's ENet peer ID tells you which connection sent an RPC; your backend player ID tells you which durable account owns a save, score, or inventory. Keep those identities explicit instead of treating a network peer number as an account.

With Crux, a shipped player build starts with a publishable key, logs the player in, and receives a player token. The Godot SDK keeps that token for player-scoped calls:

if not Crux.init_player(
    "https://crux.supercraft.host",
    PROJECT_ID,
    ENVIRONMENT_ID,
    PUBLISHABLE_KEY
):
    return

var auth := await Crux.login_anonymous()
await Crux.set_player_document("", "profile", {
    "display_name": "pilot",
    "tutorial_complete": false,
})

A trusted dedicated server uses a server token instead. That credential must never ship in the player build; it can act on behalf of a known player for server-authoritative writes:

Crux.init_server(
    "https://crux.supercraft.host",
    PROJECT_ID,
    ENVIRONMENT_ID,
    SERVER_TOKEN
)

var save := await Crux.get_player_document(player_id, "save")
await Crux.set_player_document(player_id, "save", {
    "level": 12,
    "xp": 8400,
}, save.get("version", -1))

Do not copy old examples that call a generic /validate endpoint or try to validate Crux JWTs with a public key. Crux player JWTs use HS256 and there is no public JWKS. For a self-hosted trusted server, validate the player access token online with GET /v1/auth/me; it runs the token through the normal player-auth middleware and returns the trusted project_id and player_id. Require your expected project before binding that player id to the game connection. For a Crux Runtime session, prefer the session-bound join-ticket flow below.

Runtime admission: bind a player to one live session

After player login, a client can request a short-lived ticket for a READY Crux Runtime session. The ticket is bound to that player, project, environment, and session; the authoritative process receives the per-session signing secret through its environment rather than receiving a project-wide API key.

# Player client, after login_anonymous() or login_email().
var join := await Crux.issue_runtime_join_ticket(session_id)
# Send join.ticket when connecting to join.endpoint.

That is an admission credential, not your persistence layer. Once admitted, the server still owns gameplay validation and uses its server-side credentials for trusted progression writes.

4. Deployment, Readiness and Server Lifecycle

Once the headless build works locally, the next problem is operational: where the process runs, how a session becomes joinable, how failures are detected, and how the process is stopped. Autoscaling is one possible layer on top of that lifecycle; it is not a prerequisite for every game.

You can self-manage that lifecycle or use a managed runtime. The trade-off is control versus the amount of scheduling, health, deployment, logging, and admission machinery your team owns.

Containerization (Docker and Kubernetes/Agones)

By wrapping your headless Godot Linux build in a Docker container, you can deploy it anywhere. A Dockerfile for Godot is surprisingly simple:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y ca-certificates
WORKDIR /app
COPY ./server_build/linux_server.x86_64 .
COPY ./server_build/linux_server.pck .
EXPOSE 7777/udp
CMD ["./linux_server.x86_64", "--headless"]

Agones is one self-managed option if you already want Kubernetes semantics for game-server processes. It can be a good fit for a team that wants control of the cluster and allocation layer; it is also infrastructure you must operate, observe, upgrade, and capacity-plan.

What Crux Runtime Does Today

Crux Runtime takes a different path: upload a Linux x86-64 Godot dedicated-server export as a ZIP, activate an immutable build, start an isolated process, wait for READY, then issue short-lived player join tickets. Docker, a container registry, and a cloud account are not required for that path.

Current boundary: Runtime is a capacity-gated closed alpha. Runtime v0 is one EU region, native UDP/ENet, ephemeral sessions, and one fixed server shape. It does not currently promise autoscaling, persistent worlds, web transports, or additional regions. See the live Runtime page for the current allowance and admission model.

Shutdown behavior matters whichever host you choose. Treat termination as a normal lifecycle event: stop accepting new players, flush the durable state your game requires, and exit cleanly. Crux Runtime sends SIGTERM before force-killing a process that does not stop within its grace window.

5. Matchmaking vs. Server Browsers

How do players find your Godot servers? You have two primary backend architectures to choose from:

The Matchmaker Approach

In a session-based design, players enter a queue and the backend returns a session only when there is somewhere valid to send them. Crux's matcher today is deliberately narrow: it pairs players in exact game_mode + region buckets. With a registered external fleet it selects a live heartbeating server; with a ready Runtime build it can provision an ephemeral Runtime session and expose a player-specific join ticket. It is not an MMR, party, backfill, or latency-optimization engine.

The Server Browser Approach

For survival, co-op, and sandbox games, a server browser is often the simpler product. A server registers an ID and connection metadata, heartbeats while it is alive, and deregisters on clean shutdown. Clients query the browser and render the returned live instances.

Crux.init_server(BASE_URL, PROJECT_ID, ENVIRONMENT_ID, SERVER_TOKEN)
await Crux.register_server({
    "server_id": SERVER_ID,
    "name": "EU co-op 01",
    "region": "eu",
    "ip_address": PUBLIC_IP,
    "port": 7777,
    "map_name": "forest",
    "game_mode": "coop",
    "player_count": multiplayer.get_peers().size(),
})

# Repeat while the server is live.
await Crux.heartbeat(SERVER_ID)

The shipped Godot SDK also exposes list_servers() for the browser side. Keep heartbeat cadence and stale-server cleanup as operational concerns rather than hard-coding the article's old 30-second assumption into your architecture.

6. Handling Persistent State and Databases

What happens when a player chops down a tree, builds a base, or levels up? If the server crashes, all data stored in RAM is lost. Your backend architecture must include a strategy for persistent state.

For compact player state such as progression, loadouts, and settings, versioned documents are a practical fit because the shape can evolve with the game and optimistic versions prevent blind overwrite races. That does not make JSON universally better than SQL: relational data is often the right choice when joins, constraints, reporting, or transactional relationships dominate.

Persistent world state is usually a different storage problem from a player profile. Large snapshots, chunks, or replay logs may belong in object/blob storage or a purpose-built world store, with metadata in the backend database. Choose the checkpoint cadence from your tolerated rollback window and write volume rather than copying a fixed “every five minutes” rule.

7. Dealing with Latency and Physics Integrity

Godot's MultiplayerSynchronizer is a replication tool, not a promise about the bandwidth or responsiveness of your particular game. Measure the state you replicate, its update frequency, and the number of peers before deciding you need a lower-level protocol.

Fast action games often add client-side prediction and server reconciliation so input feels immediate while the server remains authoritative. Slower co-op, tactics, card, or simulation games can make different trade-offs. Treat tick rate, snapshot rate, interpolation, and prediction as game-specific networking decisions, not requirements imposed by the backend.

Summary and Next Steps

A production Godot dedicated server is a chain of explicit contracts: the dedicated export starts headlessly, ENet carries gameplay traffic, admission decides who may join, durable storage survives process death, and lifecycle tooling decides when a session is actually ready. Keeping those contracts separate makes each one testable.

See all of it in one working project

The public Crux SDK repository includes Relay Zero, a Godot 4 co-op reference project with a browser client and headless server. Use it as inspectable implementation material, not as evidence that every game needs the same tick rate, persistence model, or deployment topology.

You can build the backend and lifecycle layers yourself or adopt only the managed pieces that remove work from your team. The useful question is not “managed or custom?” in the abstract; it is which operational contracts you are willing to own.

To see how a fully integrated backend can accelerate your Godot development, check out the Crux platform.