Hosting Godot dedicated servers
Gameye runs Godot 4 dedicated servers as Linux containers. Each match is one server process in its own Gameye session, on a dedicated server in the region you choose. Your project needs no Gameye plugin or SDK.
How it works
Section titled “How it works”- Export a dedicated server build of your project for Linux.
- Package it as a Linux container and push it to Docker Hub or GitHub Container Registry.
- Create a Gameye application for the image, with the game port registered as UDP (
7777in this guide). - When your matchmaker forms a match, your backend calls
POST /sessionand sends the players the session’s IP and port. - The server exits when the match ends. That ends the session.
Prerequisites
Section titled “Prerequisites”- A Gameye account. Sign up at trial.gameye.com.
- An API token for your backend, created in the Admin Panel under Users → API tokens.
- Godot 4 with the export templates for your exact version installed (Editor → Manage Export Templates).
- Docker.
1. Start the server
Section titled “1. Start the server”Use Godot’s high-level multiplayer with an authoritative server. The server listens on a fixed port. In bridge mode, the default, Gameye maps that port to a free port on the node.
# server_bootstrap.gd: add as an autoloadextends Node
const PORT := 7777const MAX_CLIENTS := 16
func _ready() -> void: if not OS.has_feature("dedicated_server"): return var peer := ENetMultiplayerPeer.new() var err := peer.create_server(PORT, MAX_CLIENTS) if err != OK: push_error("bind failed: %s" % err) get_tree().quit(1) return multiplayer.multiplayer_peer = peer multiplayer.peer_connected.connect(func(id): print("JOIN %d" % id)) multiplayer.peer_disconnected.connect(func(id): print("LEAVE %d" % id)) print("READY port=%d" % PORT)- Treat every
@rpc("any_peer")call as untrusted input, and never let a client set an outcome directly. print()goes to stdout, which is what the logs endpoint returns.- Set
Engine.max_fpsto your tick rate. Otherwise the headless main loop spins a CPU core flat out. - For browser clients, use
WebSocketMultiplayerPeeron a TCP port instead of ENet. Browsers on https pages need a secure WebSocket, so read the TLS section of the Colyseus and Node.js guide.
2. Export a dedicated server build
Section titled “2. Export a dedicated server build”- In Project → Export, add a Linux preset for the server.
- On the Resources tab, set Export Mode to Export as dedicated server. This strips textures and audio from the export and turns on the
dedicated_serverfeature tag that the bootstrap checks. - Export from the command line, so the same step runs in CI:
godot --headless --export-release "LinuxServer" build/game-server.x86_64Keep server code away from the stripped resources, or give the server its own entry scene. Run the build once before you containerize it:
./build/game-server.x86_64 --headlessIt should print READY port=7777 and keep running.
3. Handle the stop signal
Section titled “3. Handle the stop signal”When Gameye stops a session, for example after a DELETE /session/{id} or when its ttl runs out, the container gets SIGTERM. Godot 4 doesn’t pass SIGTERM to your scripts. In a drain test on Godot 4.7.2, Godot as the container’s main process ignored it until Docker killed it, and as a child process it died without running any script.
The fix has two parts. A small shell script in the container (next section) turns SIGTERM into a stop file. An autoload checks for that file and runs one shutdown path, which the natural match end uses too:
# match_lifecycle.gd: add as an autoloadextends Node
const STOP_FILE := "/tmp/gameye-stop" # start.sh creates it on SIGTERM
var _stopping := false
func _ready() -> void: get_tree().set_auto_accept_quit(false) var poll := Timer.new() poll.wait_time = 0.25 poll.timeout.connect(_check_stop_file) add_child(poll) poll.start()
func _check_stop_file() -> void: if FileAccess.file_exists(STOP_FILE): shutdown()
func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: # a normal quit request, not SIGTERM shutdown()
func shutdown() -> void: if _stopping: return _stopping = true print("SHUTDOWN") get_tree().create_timer(10.0).timeout.connect(get_tree().quit.bind(0)) await report_results() # tell your backend how the match ended get_tree().quit(0)
func report_results() -> void: passCall MatchLifecycle.shutdown() when the match ends. The process exits with code 0, the session ends and billing stops. Signal handling can change between Godot versions, so test the stop path again after an engine upgrade.
4. The container
Section titled “4. The container”The entrypoint is the shell script that runs Godot and turns SIGTERM into the stop file. Build the image for linux/amd64.
FROM debian:bookworm-slimRUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ && rm -rf /var/lib/apt/lists/*RUN groupadd --system --gid 10001 game \ && useradd --system --uid 10001 --gid game --create-home --home-dir /home/game --shell /usr/sbin/nologin gameWORKDIR /serverCOPY --chown=game:game build/game-server.x86_64 build/game-server.pck /server/COPY <<'EOF' /server/start.sh#!/bin/shtrap 'touch /tmp/gameye-stop' TERM INTrm -f /tmp/gameye-stopstdbuf -oL -eL /server/game-server.x86_64 --headless "$@" &child=$!wait "$child"; rc=$?while kill -0 "$child" 2>/dev/null; do wait "$child"; rc=$?; doneexit "$rc"EOFRUN chmod +x /server/game-server.x86_64 /server/start.shUSER gameENTRYPOINT ["/server/start.sh"]Keep ENTRYPOINT in this exec form so SIGTERM reaches the script. The game user has a home folder because Godot keeps its user data (user://) there and logs errors at startup without one. stdbuf -oL makes Godot write each print() line as it happens. Gameye runs containers with a terminal, so the logs endpoint is live either way, but with plain docker run or in CI, Godot holds its output in a buffer and docker logs shows nothing until it fills. The inline COPY <<'EOF' needs BuildKit, which is the default builder since Docker 23. If you export with Embed PCK, drop game-server.pck from the COPY line.
docker build --platform linux/amd64 -t mygame/godot-server:build-001 .docker run --rm -p 7777:7777/udp mygame/godot-server:build-001docker push mygame/godot-server:build-001Use a new tag for every build. Nodes that already have a tag don’t pull it again. See Tagging rules.
5. Start a session
Section titled “5. Start a session”Clients ping the regions that can run your image, and your matchmaker picks one:
curl https://api.production-gameye.gameye.net/available-location/godot-server \ -H "Authorization: Bearer $GAMEYE_API_TOKEN"When the match is ready, your backend starts a session:
curl -X POST https://api.production-gameye.gameye.net/session \ -H "Authorization: Bearer $GAMEYE_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "image": "godot-server", "location": "eu-west-1", "version": "build-001", "args": ["--", "--map=arena"], "env": { "MATCH_ID": "abc-123" }, "ttl": "1h" }'Self-serve accounts use api.sandbox-gameye.gameye.net instead of the production host. Godot keeps the arguments after -- for your game: read them with OS.get_cmdline_user_args(), and read env with OS.get_environment(). Don’t put secrets in env, because anyone in your organization who can read sessions can see it.
The response holds the node IP and the port mapping:
{ "id": "a3b2c1d0-...", "host": "203.0.113.42", "ports": [{ "type": "udp", "container": 7777, "host": 49152 }]}Players connect to host on ports[].host, not to 7777:
var peer := ENetMultiplayerPeer.new()peer.create_client(host, port) # e.g. "203.0.113.42", 49152multiplayer.multiplayer_peer = peerGameye isn’t in the network path during the match. See Custom matchmaker for region selection and player tracking.
With a coding agent
Section titled “With a coding agent”gameye-skills has a Godot server skill with these templates, and the gameye-mcp server can check your Dockerfile and start sessions for you. See AI agents.
Related
Section titled “Related”- Godot dedicated server sample: a minimal Godot 4.7 project that follows this guide, with a build script that exports in a Linux container and smoke and join tests.
- Getting started: Docker Hub or GHCR, the Admin Panel and your first API call.
- Container environment variables: every variable Gameye sets in the container.
- Exporting for dedicated servers in the Godot docs.