How to Host a Godot 4 Dedicated Server for Multiplayer Games

Export a Godot 4 dedicated server as a headless Linux build, package it in Docker, and run it on Gameye — from ENetMultiplayerPeer to live multiplayer sessions.

Roberto Sasso
CTO at Gameye

By Roberto Sasso, CTO at Gameye ·

Godot 4’s high-level multiplayer makes it genuinely easy to prototype a networked game — ENetMultiplayerPeer, RPCs, and MultiplayerSpawner get you to “two players moving on a map” in an afternoon. The hard part comes later: running an authoritative dedicated server build for real players, in the regions they live in, that scales up for launch and back down when the lobby empties.

This guide covers the full path — from configuring high-level multiplayer for a headless server, to exporting a dedicated Linux build, to running live sessions on Gameye. Godot ships no hosting platform of its own and no orchestration SDK, which is exactly why this works cleanly: Gameye orchestrates your server at the container level, so there is nothing engine-specific to integrate. If your server exports to Linux and runs in Docker, it runs on Gameye.

The steps are current for Godot 4.2 through 4.4. Earlier 4.x releases work the same way — the export-preset UI moved slightly between versions, so check your editor if a menu path differs.

What you need before starting

API URLs: sandbox is api.sandbox-gameye.gameye.net, production is api.gameye.io. Examples below use the production URL — swap in the sandbox URL during integration testing.

Gameye does not require an engine-side plugin or any changes to your game logic. Your Godot server runs as a standard Linux process inside a container, so there is no SDK to import and no lifecycle calls to add inside your project.


Step 1: Set up the server side of your multiplayer

A dedicated server is just your game running in server mode, with no window and no rendering. In Godot, you start a server peer and let the scene tree drive the simulation. Your server always binds the same fixed internal port — Gameye runs it in bridge networking and maps an external port to it automatically, so you never manage port assignment yourself. Players connect to the external host and port the Session API returns (Step 6); your binary just listens on its fixed internal port.

# autoload: ServerBootstrap.gd
extends Node

const SERVER_PORT := 7777   # fixed internal port; Gameye maps an external port to it
const MAX_CLIENTS := 64

func _ready() -> void:
    # Only run server logic in headless / dedicated-server builds.
    if not OS.has_feature("dedicated_server") and not DisplayServer.get_name() == "headless":
        return

    var peer := ENetMultiplayerPeer.new()
    var err := peer.create_server(SERVER_PORT, MAX_CLIENTS)
    if err != OK:
        push_error("Failed to start server on port %d (err %d)" % [SERVER_PORT, err])
        get_tree().quit(1)
        return

    multiplayer.multiplayer_peer = peer
    multiplayer.peer_connected.connect(_on_peer_connected)
    multiplayer.peer_disconnected.connect(_on_peer_disconnected)
    print("Godot dedicated server listening on UDP %d" % SERVER_PORT)

func _on_peer_connected(id: int) -> void:
    print("Player connected: %d" % id)

func _on_peer_disconnected(id: int) -> void:
    print("Player disconnected: %d" % id)

A note on tick rate. Godot’s default physics tick rate is 60 ticks per second (Engine.physics_ticks_per_second, set in Project Settings → Physics → Common → Physics Ticks Per Second). On an authoritative server that value is your simulation rate — every player’s state is resolved against it. 60 is a sensible default for most action games; raise it for fast-paced shooters, lower it for turn-based or slower simulations to save CPU per session. Your network send rate is separate — control how often you replicate state with a timer rather than blasting on every physics tick.

Step 2: Export a headless Linux dedicated server build

Godot 4 has a first-class dedicated server export mode that strips rendering, audio, and editor resources so the build is lean and runs without a GPU.

  1. Open Project → Export and add a Linux preset (or duplicate your existing one).
  2. Set the export mode to “Dedicated Server” (Resources tab → Export Mode). This drops textures and audio you don’t need server-side.
  3. Make sure Embed PCK is enabled so you ship a single self-contained binary.

Then export from the command line so this slots into CI:

# Godot 4.x — headless editor exporting a dedicated-server Linux build
godot --headless --export-release "Linux" ./build/game_server.x86_64

Test it locally before containerizing — it should start, print your “listening” log line, and stay running:

PORT=7777 ./build/game_server.x86_64 --headless

The --headless flag runs with no display server, which is what you want on a server host. Combined with the dedicated-server export mode, Godot never initializes rendering.

Step 3: Write the Dockerfile

The container only needs the exported binary and a minimal Linux base. Headless Godot has very few runtime dependencies.

FROM debian:bookworm-slim

# ca-certificates covers TLS if your server calls out (auth, webhooks, etc.)
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY build/game_server.x86_64 /app/game_server.x86_64
RUN chmod +x /app/game_server.x86_64

# Server binds its fixed internal port (7777); Gameye maps an external port to it.
EXPOSE 7777/udp
ENTRYPOINT ["/app/game_server.x86_64", "--headless"]

Build and run it locally to confirm the container boots and listens:

docker build -t my-godot-server:latest .
docker run --rm -p 7777:7777/udp my-godot-server:latest

If the binary complains about a missing shared library on first boot, add it to the apt-get install line. A clean dedicated-server export on bookworm-slim typically needs nothing beyond ca-certificates.

Step 4: Build and push to your registry

Tag the image for your registry and push it. Gameye pulls from any OCI-compatible registry.

docker tag my-godot-server:latest registry.example.com/yourteam/godot-server:1.0.0
docker push registry.example.com/yourteam/godot-server:1.0.0

Use immutable version tags (1.0.0, a git SHA) rather than latest so a session always runs the exact build you intend.

Step 5: Register the image with Gameye

Provide Gameye with your registry credentials and image name (e.g. yourteam/godot-server) during onboarding. Gameye pre-pulls your image onto infrastructure in every region you deploy to, so by the time your matchmaker calls the API there’s no image transfer happening — just a container start. When you push a new version tag, Gameye pulls it in the background so it’s warm before you route sessions to it.

Step 6: Start sessions from your matchmaker

When your matchmaker decides a match is ready, it calls the Gameye Session API. First, get the available locations for your image and have clients ping the latencyIp addresses so you can pick the closest region:

GET https://api.gameye.io/available-location/yourteam%2Fgodot-server
Authorization: Bearer <your-api-token>

# Response
{
  "locations": [
    { "location": "europe",    "latencyIp": "185.x.x.x" },
    { "location": "us-east",   "latencyIp": "104.x.x.x" },
    { "location": "asia-east", "latencyIp": "43.x.x.x" }
  ]
}

Then start the session in the chosen location:

POST https://api.gameye.io/session
Authorization: Bearer <your-api-token>
Content-Type: application/json

{
  "location": "europe",
  "image": "yourteam/godot-server",
  "version": "1.0.0",
  "args": ["--", "--map=arena", "--max-players=16"],
  "labels": { "match_id": "abc123", "game_mode": "ranked" },
  "ttl": 3600
}

Anything after -- is passed to your game; read it with OS.get_cmdline_user_args() in your bootstrap. The response comes back in ~0.5 seconds with the external connection details:

{
  "id": "session-abc123",
  "host": "185.x.x.x",
  "ports": [
    { "type": "udp", "container": 7777, "host": 34521 }
  ]
}

Players connect to host and the external host port from the array — not 7777. Your binary always binds 7777 internally; Gameye maps an ephemeral external port to it. Gameye is not in the network path during gameplay, so there’s no relay hop or added latency. Hand those values to your clients:

# client side — host and external port returned by the Session API
var peer := ENetMultiplayerPeer.new()
peer.create_client(host, external_port)
multiplayer.multiplayer_peer = peer

Step 7: Shut down cleanly when the match ends

When a match finishes, your server process should exit cleanly. Gameye monitors the container — when the process exits with code 0, Gameye reclaims the compute and stops billing for that session. The simplest pattern is a self-terminating server that quits once the last player leaves:

func _on_peer_disconnected(id: int) -> void:
    print("Player disconnected: %d" % id)
    if multiplayer.get_peers().is_empty():
        print("Lobby empty — shutting down")
        get_tree().quit(0)

If your backend needs to force-end a session before the server self-terminates — say a player reports a crash mid-match — call the API directly:

DELETE https://api.gameye.io/session/session-abc123
Authorization: Bearer <your-api-token>

Self-termination on empty lobby is the cheapest pattern: you only pay for a server while players are actually in it.

Taking it to production

A working session is the start. For a live Godot game, the things that matter next are:

Gameye runs dedicated servers for any engine with no SDK and no plugin — see engine support for the full picture, or how Gameye compares to other hosts if you’re evaluating options.

Frequently asked questions

Does Godot support dedicated servers?

Yes. Godot 4 has a built-in dedicated server export mode that strips rendering and audio, plus a --headless runtime flag so the server runs without a display. Combined with the high-level multiplayer API (ENetMultiplayerPeer), you get an authoritative server build with no third-party netcode required.

What is Godot’s default physics tick rate?

60 ticks per second. It’s set in Project Settings → Physics → Common → Physics Ticks Per Second, and read at runtime via Engine.physics_ticks_per_second. On a dedicated server this is your authoritative simulation rate — keep 60 for most games, raise it for fast-paced action, and lower it for slower or turn-based games to reduce CPU cost per session.

How do I run a Godot server headless?

Export a Linux build with the export mode set to Dedicated Server, then run the binary with the --headless flag (e.g. ./game_server.x86_64 --headless). No display server or GPU is needed. Inside a container, set that as the ENTRYPOINT.

Do I need an SDK or plugin to host a Godot server on Gameye?

No. Gameye orchestrates at the container level, not the engine level. Your Godot server starts, listens on its fixed internal port, and accepts connections — Gameye maps an external port to it and handles session allocation, region placement, scaling, and shutdown externally via its REST API. Nothing runs inside your game binary.

ENet, WebSocket, or WebRTC for a Godot dedicated server?

For native desktop and mobile clients, ENet (ENetMultiplayerPeer) over UDP is the default and the right choice — low latency, reliable/unreliable channels. For browser-based clients use WebSocketMultiplayerPeer. Both expose the same high-level multiplayer API, and both run identically on Gameye — only the listen protocol in your image registration changes.

How much does it cost to host a Godot multiplayer game?

You pay for sessions while they’re running, not for an idle fleet. Cost is driven by concurrent active sessions and the CPU each one uses (your tick rate and game logic), not by registered players. See pricing for current rates, and use server sizing to estimate your footprint.