From Unity Sample to Gameye-Hosted Dedicated Server in an Afternoon

A step-by-step walkthrough taking Photon Fusion's SimpleFPS from local build to Gameye-hosted dedicated server, covering Docker, the Session API, and a Quick Play broker.

Andrew Walker
CBO at Gameye

By Andrew Walker, CBO at Gameye ·

Most multiplayer samples stop at “it runs locally.” That is not where production starts.

For a real dedicated server flow, a developer needs to get from a game engine project to a Linux server build, then into a Docker image, then into a registry, then into an orchestration platform, then back into the game client through matchmaking or a session broker.

We took Photon Fusion’s SimpleFPS Unity sample through that path and got it running on Gameye.

The first milestone used a deterministic room code. The final demo removed the manual start step and wired Quick Play through a small broker that starts or reuses a Gameye session automatically.

Unity SimpleFPS project
  -> Linux dedicated server build
  -> Docker image
  -> DockerHub push
  -> Gameye application
  -> Gameye Session API v2
  -> Quick Play broker
  -> Unity client joins Gameye-hosted server

Final tested image and Gameye values:

Image:               andwalk/simplefps-server:dev
Gameye application:  SimpleFPS
Gameye location:     us-east-2
Photon region:       us
Container UDP port:  7777
Demo TTL:            10m

Why we did this

Gameye’s job is to remove infrastructure work from multiplayer game development. A studio should package a game server as a Docker container, call an API when a match is ready, and get a dedicated server back fast enough that players do not care where the infrastructure work happened.

But the first deployment still has sharp edges.

A developer has to understand Unity builds, Linux runtime behavior, Docker, registries, image tags, UDP ports, environment variables, and orchestration settings before the first player connects. That is a lot of surface area for what should be a one-time setup step.

This exercise had two goals:

  1. Prove the SimpleFPS dedicated server can run on Gameye.
  2. Capture the friction a developer hits before they get there.

Starting point

The project was Photon Fusion SimpleFPS running locally on an Omarchy / Arch Linux machine.

Unity Editor:        2022.3.11f1
Game sample:         Photon Fusion SimpleFPS
Local OS:            Omarchy / Arch Linux
Target runtime:      Ubuntu 22.04 container
Container protocol:  UDP
Game port:           7777

We first verified the normal Unity client build. Then we added a dedicated server path that can start without the menu UI.


Step 1: Add a dedicated server bootstrap

SimpleFPS needed a headless startup path.

The server now starts in dedicated mode when launched with one of these flags or environment variables:

-dedicatedServer
-server
-batchmode
SIMPLEFPS_DEDICATED_SERVER=1

It accepts configuration from command-line arguments or environment variables:

ROOM_NAME or GAMEYE_SESSION_ID
PORT
MAX_PLAYERS
PHOTON_REGION
PHOTON_APP_ID
PHOTON_APP_VERSION
SCENE_NAME
TARGET_FRAME_RATE

For the first validation, the important values were:

ROOM_NAME=ABCDCEFG
PHOTON_REGION=us
PORT=7777
MAX_PLAYERS=16

The server starts Fusion in server mode, binds UDP port 7777, and loads the Deathmatch scene.

Useful log markers:

[DedicatedServer] Started. Session=ABCDCEFG Region=us Port=7777
[DedicatedServer] Player joined: [Player:2]
[DedicatedServer] Player left: [Player:2]

Those logs matter. Without them, it is easy to think a client joined the intended server when it actually joined another Photon session.


Step 2: Build the Linux server

The working build path was the Unity Editor menu:

SimpleFPS -> Build -> Linux Dedicated Server

Output:

Builds/LinuxServer/SimpleFPSServer.x86_64

One practical note: a scripted Unity batchmode build crashed on this Linux setup, while the Editor menu build worked. That is not unusual. Unity automation paths can behave differently across operating systems, editor versions, and installed modules.

For a production pipeline, batchmode build reliability needs separate validation. For this demo, the Editor menu build was enough.


Step 3: Test before Docker

Before adding Docker, we ran the server directly:

cd Builds/LinuxServer
./SimpleFPSServer.x86_64 \
  -batchmode \
  -nographics \
  -dedicatedServer \
  -room ABCDCEFG \
  -region us \
  -port 7777 \
  -maxPlayers 16 \
  -logFile server.log

The Unity client joined by party code: ABCDCEFG

This was more reliable than Quick Match for early validation. Quick Match can create or join another session, which hides whether the intended dedicated server is actually working.


Step 4: Dockerize the server

The Docker artifacts were intentionally simple:

docker/Dockerfile
docker/entrypoint.sh
.dockerignore
scripts/docker-build-server.sh
scripts/docker-run-server.sh

The image uses Ubuntu 22.04 as the base image to match the target server runtime.

Important details:

Local build:

./scripts/docker-build-server.sh

Local run:

ROOM_NAME=ABCDCEFG \
PHOTON_REGION=us \
PORT=7777 \
MAX_PLAYERS=16 \
./scripts/docker-run-server.sh

Watch logs:

docker logs -f simplefps-server

The Dockerized server worked locally. The client joined the deterministic room code, and the container logs showed join/disconnect events.


Step 5: Push to DockerHub

The local image was tagged into the DockerHub namespace:

docker tag simplefps-server:local andwalk/simplefps-server:dev
docker push andwalk/simplefps-server:dev

Pushed image:

Repository:       andwalk/simplefps-server
Tag:              dev
Digest:           sha256:8fc54ca448b401c8cea0b737c0339bd7...
Compressed size:  ~158 MB

This is a common source of confusion. The local image name and the registry image name are different things. A developer needs to understand why simplefps-server:local becomes andwalk/simplefps-server:dev before Gameye can pull it.

A tool should make that explicit.


Step 6: Create the Gameye application

In the Gameye Admin Panel, the application was configured with:

Application name:  SimpleFPS
Repository URL:    andwalk/simplefps-server
Tags to keep:      1
Network mode:      bridge
Port binding:      UDP 7777
io_uring:          disabled

We skipped DockerHub webhooks for the first test and loaded tags manually.

That was the right call for a first deployment. Manual tag loading was much simpler than setting up a cross-site webhook with an API token embedded in the URL. The UI updated automatically when the tag was available.

For new users, webhook setup should be optional and clearly separated from the minimum path.


Step 7: Start a Gameye test container

For the first Gameye test, the container was started manually from the Admin Panel with environment variables:

ROOM_NAME=ABCDCEFG
PHOTON_REGION=us
PORT=7777
MAX_PLAYERS=16

Arguments were left empty because the Docker entrypoint already supplies:

-batchmode -nographics -dedicatedServer -logFile -

The Unity client joined with room code: ABCDCEFG

That proved the core path:

Unity dedicated server
  -> DockerHub image
  -> Gameye container
  -> playable client connection

Step 8: Remove the manual start step

A good demo should not require opening the Admin Panel, manually starting a container, copying a room code, and then joining it from the client.

So we added a small broker in front of Gameye.

Unity Quick Play
  -> broker /quickmatch
  -> Gameye Session API v2
  -> SimpleFPS container
  -> Photon/Fusion room returned to Unity

For this demo, the broker is a Cloudflare Worker. It can run locally with Wrangler and supports two modes:

BROKER_MODE=static
BROKER_MODE=gameye

Static mode validates the Unity client path without touching Gameye.

Gameye mode calls the Session API v2, starts or reuses a session, and returns the Photon room information to the client.

The Unity plugin only intercepts Quick Play. Explicit party-code joins still use the stock SimpleFPS path.

The details that mattered

The Gameye location and Photon region are separate values:

Gameye location:   us-east-2
Photon region:     us

The Gameye image name is also not the DockerHub repository name:

Gameye image/application:  SimpleFPS
DockerHub repository:      andwalk/simplefps-server

That distinction is easy to miss. It should be clearer in tooling and UI copy.

The Session API TTL expects a duration string:

GAMEYE_SESSION_TTL=10m

10m worked. 1hr did not. Valid units include strings like 5m, 1h, and 4h50m.

Final Quick Play result

The final smoke test used the Unity client’s Quick Play button.

Observed broker flow:

POST /quickmatch       200 OK
POST /player/join      403 Forbidden

The Quick Match call succeeded. Gameye created a session and the Unity client connected and disconnected successfully.

Session:   TS6EC85I
Location:  us-east-2
Port map:  7777/udp -> 21836
Status:    running -> exited (after cleanup)

The player tracking call failed for a known reason: the API token did not have session:write scope.

The player tracking implementation is in place as best-effort. It calls:

PUT    /session/player/join
DELETE /session/player/leave

But the current Admin Panel token UI did not expose the required session:write scope during testing. Until that scope is available, the demo still works, but Gameye player counts remain at zero.


Why this is a useful template

This is not a universal Dockerfile for every Unity game. It is a working Unity/Photon/Fusion reference.

It shows:

A different project will need different executable names, ports, build paths, environment variables, and startup logic. But the shape is reusable.


Final result

By the end of the session, SimpleFPS could run as a Gameye-hosted dedicated server and the Unity client could join it through Quick Play.

That is the important milestone.