Source: https://gameye.com/blog/deploy-mirror-fishnet-dedicated-server/

---
title: "Deploy a Mirror or FishNet Dedicated Server: Step-by-Step Guide | Gameye"
description: "Build a headless Linux server from Unity, put it in a Docker container and start sessions from your matchmaker. Build settings, a Dockerfile and API calls for Mirror and FishNet."
---

# How to Deploy a Unity Mirror or FishNet Dedicated Server (Step by Step)

A Mirror or FishNet dedicated server is a headless Linux build of your Unity project. Here is how to build it, containerize it, push it to a registry and start sessions from your matchmaker.

[Andrew Walker](https://gameye.com/blog/author/andrew-walker/)
_Head of Business Development at Gameye_

Oct 13, 2026

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_SERVER` in 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 &#x3C;= 1)
Application.Quit(0);
}
#endif
}
```

The check is `&#x3C;= 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) &#x26;&#x26; 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 &#x26;&#x26; apt-get install -y --no-install-recommends \
ca-certificates \
&#x26;&#x26; rm -rf /var/lib/apt/lists/* \
&#x26;&#x26; 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 &#x3C;your-org>/&#x3C;your-image>:build-001 .
docker run --rm -p 7777:7777/udp &#x3C;your-org>/&#x3C;your-image>:build-001
```

See [create and push your Dockerfile](https://gameye.com/docs/getting-started/2-docker-file/) 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 &#x3C;your-org>/&#x3C;your-image>:build-001
```

**GitHub Container Registry.** The image owner must match your Gameye organization name, as in `ghcr.io/&#x3C;your-org>/&#x3C;your-image>`. Give the GitHub account `gameyedocker` Read access to the package. The package can stay private. The [GHCR guide](https://gameye.com/docs/getting-started/ghcr/) 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/&#x3C;your-application>`.

```
curl -X POST https://api.production-gameye.gameye.net/session \
-H "Authorization: Bearer &#x3C;your-token>" \
-H "Content-Type: application/json" \
-d '{
"image": "&#x3C;your-application>",
"location": "&#x3C;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](https://gameye.com/docs/api-v2/reference/post-session/).

## 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](https://gameye.com/mirror-integration/) and [FishNet integration](https://gameye.com/fishnet-integration/) pages, or the [getting started guide](https://gameye.com/docs/getting-started/).

## Summary

- **Build** for the Linux Dedicated Server target. Unity defines `UNITY_SERVER` in 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 /session`** from your matchmaker and hand players the returned host and port.

To try it with your own server, [start a free trial](https://trial.gameye.com/) and run your first Mirror or FishNet session, or [talk to an engineer](https://gameye.com/contact-us/) about your setup.
