To deploy a Mirror or FishNet dedicated server, build your Unity project for the Linux Dedicated Server target, put the build in a Docker container, and start one container per match from your matchmaker. Neither library needs a hosting plugin, because the server is a normal headless Linux program that listens on one UDP port.
This guide walks through each step, with code for both libraries.
The short answer
- Build the project for Dedicated Server on Linux. Unity defines
UNITY_SERVERin that build and strips rendering. - Make the server start listening on boot, on a fixed port.
- Copy the build into a Docker image based on Linux x86-64.
- Push the image to Docker Hub or GitHub Container Registry with a new tag for every build.
- When a match is ready, your matchmaker calls
POST /session. Gameye returns an IP and port, and players connect straight to your server.
Mirror and FishNet server settings compared
These are the settings that matter on a dedicated server. The defaults come from each library’s current source.
| Setting | Mirror | FishNet |
|---|---|---|
| Default transport | KCP, over UDP | Tugboat, LiteNetLib over UDP |
| Default listen port | 7777 | 7770 |
| Auto start on a headless build | NetworkManager Headless Start Mode, default Do Nothing. Set it to Auto Start Server | ServerManager Start On Headless, on by default |
| Start the server in code | NetworkManager.singleton.StartServer() | InstanceFinder.ServerManager.StartConnection() |
| Set the port in code | PortTransport.Port on the transport | Transport.SetPort(port) or ServerManager.StartConnection(port) |
| Network rate | NetworkManager Send Rate, default 60 | TimeManager Tick Rate, default 30 |
| Player cap | NetworkManager Max Connections, default 100 | Tugboat Maximum Clients, default 4095 |
| Disconnect hook | Override NetworkManager.OnServerDisconnect | ServerManager.OnRemoteConnectionState |
Lower the player cap to your real match size. The default is far higher than one match needs.
Build the Linux dedicated server
In Unity Hub, add the Linux Dedicated Server Build Support module to your editor. Then pick Linux Server in Build Profiles, or Dedicated Server with Linux in Build Settings on older versions, and build to Build/.
For CI, add a small editor script and call it from the command line:
using UnityEditor;
public static class BuildScript
{
public static void BuildDedicatedServer()
{
BuildPipeline.BuildPlayer(new BuildPlayerOptions
{
scenes = new[] { "Assets/Scenes/Main.unity" },
locationPathName = "Build/GameServer.x86_64",
target = BuildTarget.StandaloneLinux64,
subtarget = (int)StandaloneBuildSubtarget.Server,
});
}
}
"$UNITY" -batchmode -quit -nographics \
-projectPath "$PWD" \
-buildTarget Linux64 -standaloneBuildSubtarget Server \
-executeMethod BuildScript.BuildDedicatedServer \
-logFile -
Run the binary on a Linux machine with -batchmode -nographics before you containerize it, and fix any startup errors there first.
Start Mirror in server mode
On the NetworkManager, set Headless Start Mode to Auto Start Server and keep the KCP port at 7777. Mirror then calls StartServer() when the headless build loads.
Add a shutdown hook, so the container exits when the match empties and the session stops billing:
using Mirror;
using UnityEngine;
public class GameNetworkManager : NetworkManager
{
public override void OnServerDisconnect(NetworkConnectionToClient conn)
{
base.OnServerDisconnect(conn);
#if UNITY_SERVER
// Mirror removes the connection before this callback runs.
if (NetworkServer.connections.Count == 0)
Application.Quit(0);
#endif
}
}
In a real game, add a short grace period so a brief disconnect does not end a match in progress.
Start FishNet in server mode
FishNet’s ServerManager has Start On Headless enabled by default. A Linux Dedicated Server build starts Tugboat on port 7770 as soon as the NetworkManager loads, so you need no start code.
The shutdown hook looks like this:
using FishNet;
using FishNet.Connection;
using FishNet.Transporting;
using UnityEngine;
public class ServerShutdown : MonoBehaviour
{
#if UNITY_SERVER
private void Start()
{
InstanceFinder.ServerManager.OnRemoteConnectionState += OnRemoteConnectionState;
}
private void OnRemoteConnectionState(NetworkConnection conn, RemoteConnectionStateArgs args)
{
if (args.ConnectionState != RemoteConnectionState.Stopped)
return;
// FishNet raises this event before it removes the client from Clients.
if (InstanceFinder.ServerManager.Clients.Count <= 1)
Application.Quit(0);
}
#endif
}
The check is <= 1 and not == 0. FishNet still counts the leaving client when it raises the event.
How the server finds its port
Gameye offers two network modes, and you choose one per application.
- Bridge is the default to pick if you are unsure. Your server listens on a fixed container port, such as UDP 7777 for Mirror or UDP 7770 for FishNet. Gameye maps it to a free port on the node. The code above works unchanged.
- Host gives the container the node’s network directly, so the server must listen on the port Gameye assigns. Gameye passes it as
GAMEYE_PORT_{PROTOCOL}_{CONTAINER_PORT}.
Gameye sets that variable in both modes, so read it only when you run in host mode. In bridge mode it holds the public port, not the one to bind. For Mirror in host mode:
public override void Start()
{
string value = System.Environment.GetEnvironmentVariable("GAMEYE_PORT_UDP_7777");
if (ushort.TryParse(value, out ushort port) && transport is PortTransport pt)
pt.Port = port;
base.Start(); // Headless Start Mode then starts the server
}
For FishNet in host mode, turn off Start On Headless and start the server yourself with InstanceFinder.ServerManager.StartConnection(port), using GAMEYE_PORT_UDP_7770.
Write the Dockerfile
A headless Unity server needs very little from the base image. This one runs as a non-root user and starts the server directly:
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd -m gameuser
WORKDIR /app
COPY --chown=gameuser:gameuser Build/ /app/
RUN chmod +x /app/GameServer.x86_64
USER gameuser
# Mirror KCP listens on 7777/udp. For FishNet Tugboat, use 7770/udp.
EXPOSE 7777/udp
ENTRYPOINT ["/app/GameServer.x86_64", "-batchmode", "-nographics"]
Gameye nodes run linux/amd64. On an Apple silicon Mac, build for that platform and test locally:
docker build --platform linux/amd64 -t <your-org>/<your-image>:build-001 .
docker run --rm -p 7777:7777/udp <your-org>/<your-image>:build-001
See create and push your Dockerfile for the full template and the port table.
Push the image to Docker Hub or GHCR
Use a new tag for every build, such as a build number or a commit SHA. Nodes cache images by tag, so pushing latest again does not deliver your new build.
Docker Hub. Push the image, add the user gameyedocker as a read-only collaborator on a private repository, and add the webhook URL Gameye gives you, so each new tag registers itself:
docker push <your-org>/<your-image>:build-001
GitHub Container Registry. The image owner must match your Gameye organization name, as in ghcr.io/<your-org>/<your-image>. Give the GitHub account gameyedocker Read access to the package. The package can stay private. The GHCR guide includes a GitHub Actions workflow that publishes a unique tag on every push.
Then create an application in the Admin Panel. Point it at the repository and declare the UDP port your server listens on.
Start sessions from your matchmaker with POST /session
When your matchmaker has a full lobby, it asks Gameye for a server. Only image and location are required. image is your application name. location is a region ID from GET /available-location/<your-application>.
curl -X POST https://api.production-gameye.gameye.net/session \
-H "Authorization: Bearer <your-token>" \
-H "Content-Type: application/json" \
-d '{
"image": "<your-application>",
"location": "<region-id>",
"ttl": "1h",
"env": { "MATCH_MODE": "ranked" },
"labels": { "match": "1234" }
}'
Trial accounts call api.sandbox-gameye.gameye.net instead. The 201 response carries the address:
{
"id": "f7965a22-e327-4541-a30e-8262890d3a29",
"host": "203.0.113.42",
"ports": [{ "type": "udp", "container": 7777, "host": 49152 }]
}
Send host and ports[].host to your players. On the client, set Mirror’s NetworkManager.networkAddress and the transport port, or Tugboat’s client address and port, and connect. Players connect to the external port from the response, not to 7777 or 7770.
Set a ttl on every session as a safety net, and call DELETE /session/{id} when the match ends. The full field list is in the POST /session reference.
Test and scale
Start with a short ttl, such as "10m", while you test. Start a session, connect two editor clients to the returned address, then leave and check that the container exits.
If players cannot connect, check the port first. In bridge mode, the application’s port binding must match the port your transport listens on. In host mode, the server must bind the GAMEYE_PORT_* value.
Scaling needs no extra work in Unity. Each match is one session, and your matchmaker calls POST /session once per match. Sessions start in 0.5 seconds on average, and there is no Gameye SDK to add to your server. Compute costs $0.07 per vCPU-hour on demand and $0.035 reserved, with no egress fees.
For library-specific notes, see the Mirror integration and FishNet integration pages, or the getting started guide.
Summary
- Build for the Linux Dedicated Server target. Unity defines
UNITY_SERVERin that build. - Start on boot. Set Mirror’s Headless Start Mode to Auto Start Server. FishNet starts on headless by default.
- Keep the port fixed in bridge mode. Mirror KCP uses 7777 and FishNet Tugboat uses 7770.
- Exit when the match empties, so the session stops billing.
- Push a unique tag for every build to Docker Hub or GHCR.
- Call
POST /sessionfrom your matchmaker and hand players the returned host and port.
To try it with your own server, start a free trial and run your first Mirror or FishNet session, or talk to an engineer about your setup.