How to Containerize and Deploy an Unreal Engine 5 Dedicated Server

A practical guide to packaging a UE5 dedicated server as a Docker container and running it through Gameye's orchestration platform — from Linux build to live sessions.

Roberto Sasso
CTO at Gameye

Mar 30, 2026

Running an Unreal Engine 5 dedicated server at scale requires two things: a reliable build pipeline that produces a portable server binary, and an orchestration layer that starts sessions on demand, places them close to players, and tears them down when matches end. Containers solve the first problem. Gameye handles the second.

This guide walks through the complete process — from compiling a Linux headless server build to calling the Gameye Session API from your matchmaker.

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 server binary. Your UE5 server runs as a standard Linux process inside a container, so no SDK integration, no GameLift Server SDK to remove, and no rebuild of server logic.


Step 1: Compile the Linux dedicated server target

In your UE5 project, add the dedicated server target file if it doesn’t exist already. In Source/, alongside your existing <ProjectName>.Target.cs, add:

// Source/MyGameServer.Target.cs
using UnrealBuildTool;

public class MyGameServerTarget : TargetRules
{
    public MyGameServerTarget(TargetInfo Target) : base(Target)
    {
        Type = TargetType.Server;
        DefaultBuildSettings = BuildSettingsVersion.V4;          // UE 5.3+ — adjust to your engine version
        IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_3; // UE 5.3 — adjust to your engine version
        ExtraModuleNames.Add("MyGame");
    }
}

Build the Linux server binary from your build machine:

./Engine/Build/BatchFiles/RunUAT.sh BuildCookRun \
  -project="/path/to/MyGame.uproject" \
  -noP4 \
  -platform=Linux \
  -serverconfig=Shipping \
  -server \
  -build \
  -cook \
  -stage \
  -pak \
  -archive \
  -archivedirectory="/output/LinuxServer"

This produces a staged directory at /output/LinuxServer/ containing your headless server binary and all packaged content. The binary itself will be at a path like LinuxServer/MyGame/Binaries/Linux/MyGameServer.

Test it locally before containerizing:

./LinuxServer/MyGame/Binaries/Linux/MyGameServer \
  /Game/Maps/Gameplay \
  -port=7777 \
  -log

If it starts, listens on UDP 7777, and logs session activity, it’s ready to containerize.


Step 2: Write the Dockerfile

The goal is a minimal, reproducible image. UE5 server binaries require glibc and a handful of system libraries — Ubuntu 22.04 LTS provides a clean base without unnecessary overhead.

FROM ubuntu:22.04

# Install runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    libssl3 \
    libicu70 \
    ca-certificates \
    && rm -rf /var/lib/apt/lists/*

# Create a non-root user — UE5 refuses to run as root
RUN useradd -ms /bin/bash gameserver
USER gameserver
WORKDIR /home/gameserver

# Copy the staged server build
COPY --chown=gameserver:gameserver LinuxServer/ ./

# Document the game port — EXPOSE is metadata only, does not open or publish the port
# With host networking (used in production), this has no effect
EXPOSE 7777/udp

# Entrypoint — map and arguments come in via the Gameye Session API
ENTRYPOINT ["./MyGame/Binaries/Linux/MyGameServer"]

Key points:

Non-root user is required. Unreal Engine applications refuse to run as root, so the useradd step is not optional.

UDP not TCP. Unreal Engine uses UDP for client-server communication. Make sure your registry and cloud firewall rules account for this — it’s a common gotcha when migrating from services that default to TCP.

No launch arguments in the Dockerfile. Map name, game mode, max player count, and any custom flags are passed at session start time via the Gameye args parameter. This means one image supports multiple game modes and maps without rebuilding.

Bridge vs. host networking. The Dockerfile above works with Gameye’s default bridge networking mode: your server binds to container port 7777, and Gameye maps an ephemeral external port to it. The external port is returned in the session response — pass that to players, not 7777 directly. If you configure host networking instead (lower latency for UDP), Gameye injects the assigned port via a GAMEYE_PORT_UDP_7777 environment variable and your server entrypoint must read and bind to it rather than hardcoding 7777. Bridge networking is the simpler starting point for most studios.


Step 3: Build and push to your registry

# Build
docker build -t registry.example.com/mygame/server:1.0.0 .

# Test locally
docker run --rm \
  --network host \
  registry.example.com/mygame/server:1.0.0 \
  /Game/Maps/Gameplay -port=7777 -log

# Push
docker push registry.example.com/mygame/server:1.0.0

Using --network host for local testing avoids the latency overhead that container NAT introduces for UDP. In Gameye’s infrastructure, servers run with host networking by default for the same reason.

Tag your images with a version string, not just latest. When you register the image with Gameye, you can deploy specific versions to production while testing newer builds in sandbox — this becomes important during live operations when you need to roll back quickly.


Step 4: Register the image with Gameye

Provide Gameye with your registry credentials and image name during onboarding. Gameye pre-pulls your image onto infrastructure in every region you’re deploying to. This pre-pull step is what makes sub-second session starts achievable — by the time your matchmaker calls the API, there’s no image transfer happening, just a container start.

If you push a new image version, Gameye pulls the updated tag in the background so it’s warm and ready before you start routing sessions to it.


Step 5: Start sessions from your matchmaker

When your matchmaker decides a match is ready, it calls the Gameye Session API. First, get available locations for your image:

GET https://api.gameye.io/available-location/mygame%2Fserver
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" }
  ]
}

Have your game clients ping those latencyIp addresses before matchmaking. Aggregate the results in your matchmaker to pick the optimal location, then start the session:

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

{
  "location": "europe",
  "image": "mygame/server",
  "version": "1.0.0",
  "args": ["/Game/Maps/Gameplay", "-maxplayers=10"],  // omit -log in shipping builds
  "labels": {
    "match_id": "abc123",
    "game_mode": "ranked"
  },
  "ttl": 3600
}

The response comes back in ~0.5 seconds:

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

Pass host (the IP) and the host port value from the ports array to your players. They connect directly — Gameye is not in the network path during gameplay, so there’s no relay hop or added latency.

Note that the host port in the response is the external port players connect to — it will not necessarily be 7777. Gameye auto-assigns an ephemeral external port (32768–60299) in bridge networking mode and maps it to your container’s internal port 7777. Your server binary always binds to 7777 internally; players use whatever port the response returns.

The labels field is useful for correlating sessions with your own match IDs in observability tooling. The ttl field sets a hard maximum session lifetime in seconds — useful for capping runaway sessions. It’s mutually exclusive with restart.


Step 6: Handle session shutdown

When the match ends, your server process should exit cleanly. Gameye monitors the container — when the process exits, Gameye reclaims the compute and stops billing for that session.

The simplest pattern is a self-terminating server: when the last player disconnects (or a custom match-end timer fires), the server calls FPlatformMisc::RequestExit(false) or your equivalent shutdown path, the process exits with code 0, and Gameye handles cleanup.

If your backend needs to force-end a session before the server self-terminates — for example, when a player reports a crash mid-match — call:

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

This stops the container immediately regardless of process state.


Taking it to production

A few patterns that matter at scale:

Version your images in CI. A typical pipeline tags images with the git commit SHA and a semver release tag. When you cut a release, push both — Gameye can run them simultaneously so you can validate the new version under real load before shifting all traffic to it.

Use labels for observability. Tag every session with match_id, game_mode, and build_version. This makes it possible to correlate infrastructure metrics with game-specific events when something goes wrong.

Pre-warm for launch peaks. Before a content launch or free weekend, call the Gameye pre-warm endpoint to ensure capacity is staged and ready. The Gameye team can help with capacity planning for known events.

Test the shutdown path. A session that never terminates — because the shutdown logic has a bug — burns compute indefinitely. Test your match-end path in sandbox before going live, and set a ttl as a backstop.


Containerizing a UE5 server doesn’t require any engine-specific integration. If your server binary runs on Linux and communicates over UDP, it runs on Gameye. The Dockerfile above is the entire portability layer.


Related: Unreal Engine dedicated server hosting on Gameye · How Gameye orchestration works · Gameye vs AWS GameLift — for studios moving off cloud-only platforms · Matchmaker integrations · Get started with the Gameye API →