Skip to content

Hosting Unity dedicated servers

Gameye runs Unity 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 package or SDK. Any Unity netcode stack with a dedicated server mode works, including Netcode for GameObjects, FishNet, Mirror and Photon Fusion.

  1. Build your project with the Linux Dedicated Server target.
  2. Package the build as a Linux container and push it to Docker Hub or GitHub Container Registry.
  3. Create a Gameye application for the image, with the game port registered as UDP (7777 in this guide).
  4. When your matchmaker forms a match, your backend calls POST /session and sends the players the session’s IP and port.
  5. The server exits when the match ends. That ends the session.
  • A Gameye account. Sign up at trial.gameye.com.
  • An API token for your backend, created in the Admin Panel under Users → API tokens.
  • Unity 2021.2 or later with the Linux Dedicated Server Build Support module installed for your Editor version. Install the IL2CPP module too if you build with IL2CPP.
  • Docker.

The server listens on all interfaces on a fixed port. In bridge mode, the default, Gameye maps that port to a free port on the node. With Netcode for GameObjects and Unity Transport:

// ServerBootstrap.cs: put it on an object in your first scene
using Unity.Netcode;
using Unity.Netcode.Transports.UTP;
using UnityEngine;
public class ServerBootstrap : MonoBehaviour
{
const ushort Port = 7777;
#if UNITY_SERVER
void Start()
{
Application.targetFrameRate = 60; // your tick rate
QualitySettings.vSyncCount = 0;
var transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
transport.SetConnectionData("0.0.0.0", Port, "0.0.0.0");
NetworkManager.Singleton.OnClientConnectedCallback += id => Debug.Log($"JOIN {id}");
NetworkManager.Singleton.OnClientDisconnectCallback += id => Debug.Log($"LEAVE {id}");
if (!NetworkManager.Singleton.StartServer())
{
Debug.LogError("server failed to start");
Application.Quit(1);
return;
}
Debug.Log($"READY port={Port}");
}
#endif
}
  • Set Application.targetFrameRate to your tick rate. A dedicated server build has no vsync, so without it the main loop spins a CPU core flat out.
  • The Dedicated Server target defines UNITY_SERVER. Use #if UNITY_SERVER and #if !UNITY_SERVER so one project builds both the client and the server.
  • With FishNet, Mirror or another stack, the shape is the same: start the server on a fixed UDP port and bind to all interfaces. For Photon Fusion, see Using Photon Fusion.
  • Debug.Log goes to stdout once you start the player with -logFile - (see the container section). Stdout is what the logs endpoint returns.
  • Treat every message from a client as untrusted input, and never let a client set an outcome directly.
  1. In Unity 6, open File → Build Profiles and choose Linux Server. In Unity 2021.2 to 2023, open File → Build Settings, choose Dedicated Server and set the target platform to Linux. This is the Dedicated Server target. It strips rendering and audio from the build. The older “Server Build” or headless checkbox on the Linux player is not the same thing.
  2. Prefer the IL2CPP scripting backend for production servers. Mono is fine while you iterate.
  3. Build from the command line, so the same step runs in CI. BuildScript.BuildServer is your own editor script that calls BuildPipeline.BuildPlayer with StandaloneBuildSubtarget.Server:
Terminal window
unity -batchmode -quit -projectPath . \
-buildTarget Linux64 -standaloneBuildSubtarget Server \
-executeMethod BuildScript.BuildServer

The output is a binary such as GameServer.x86_64, a GameServer_Data folder and UnityPlayer.so. Run it once before you containerize it:

Terminal window
./Build/LinuxServer/GameServer.x86_64 -batchmode -nographics -logFile -

It should print READY port=7777 and keep running.

When Gameye stops a session, for example after a DELETE /session/{id} or when its ttl runs out, the container gets SIGTERM. The Unity player handles it as a normal quit, so you don’t need a wrapper script. In our test, a Unity 6000.3 IL2CPP dedicated server running as the container’s main process exited with code 0 about 2.4 seconds after SIGTERM, with its logs flushed. If Unity is still running about 50 seconds later, the container is killed.

Put your end-of-match work in one handler and use it for both paths, the stop signal and the natural match end:

void Awake()
{
Application.quitting += ReportResultsAndFlush;
}
// When the match ends:
// Application.Quit(0);
void ReportResultsAndFlush()
{
// Tell your backend how the match ended. Keep it to a few seconds.
}

When the match ends, quit. The process exits with code 0, the session ends and billing stops. Don’t go back to a lobby scene: start a new session for the next match. Signal handling can change between Unity versions, so after an engine upgrade, run docker stop on a local container and check that your handler still runs.

Start the player directly as the container’s main process, so SIGTERM reaches Unity. Build the image for linux/amd64.

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libstdc++6 libgcc-s1 libatomic1 zlib1g \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system --gid 10001 game \
&& useradd --system --uid 10001 --gid game --no-create-home --shell /usr/sbin/nologin game
WORKDIR /server
COPY --chown=game:game Build/LinuxServer/ /server/
RUN chmod +x /server/GameServer.x86_64
USER game
EXPOSE 7777/udp
ENTRYPOINT ["/server/GameServer.x86_64", "-batchmode", "-nographics", "-logFile", "-"]
  • -logFile - sends the Unity player log to stdout. Without it, the log goes to a file inside the container and the logs endpoint shows nothing.
  • Keep ENTRYPOINT in this exec form. If you need a shell script to turn environment variables into Unity arguments, end it with exec so Unity replaces the shell and still gets SIGTERM.
  • Add a .dockerignore that keeps Library/, Temp/ and the other Unity folders out of the build context.
Terminal window
docker build --platform linux/amd64 -t mygame/unity-server:build-001 .
docker run --rm -p 7777:7777/udp mygame/unity-server:build-001
docker push mygame/unity-server:build-001

Use a new tag for every build. Nodes that already have a tag don’t pull it again. See Tagging rules.

Clients ping the regions that can run your image, and your matchmaker picks one:

Terminal window
curl https://api.production-gameye.gameye.net/available-location/unity-server \
-H "Authorization: Bearer $GAMEYE_API_TOKEN"

When the match is ready, your backend starts a session:

Terminal window
curl -X POST https://api.production-gameye.gameye.net/session \
-H "Authorization: Bearer $GAMEYE_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"image": "unity-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. Gameye appends args to your ENTRYPOINT, so read them with System.Environment.GetCommandLineArgs(). Read env with System.Environment.GetEnvironmentVariable(). 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 transport = NetworkManager.Singleton.GetComponent<UnityTransport>();
transport.SetConnectionData(host, port); // e.g. "203.0.113.42", 49152
NetworkManager.Singleton.StartClient();

Gameye isn’t in the network path during the match. See Integrating Unity Matchmaker to start sessions from Unity Matchmaker, or Custom matchmaker for region selection and player tracking.

gameye-skills has a Unity server skill with these templates, and the gameye-mcp server can check your Dockerfile and start sessions for you. See AI agents.