View as Markdown

How to deploy game servers with Edgegap

This guide walks you through integrating Nakama with Edgegap using the open-source Nakama-Edgegap plugin. You will configure your Nakama server to use Edgegap as its fleet manager, set up a headless Unity game server, and connect the two so that Nakama’s matchmaker can automatically deploy dedicated game server instances on Edgegap’s edge network.

By the end, your Nakama server will be able to create, manage, and tear down Edgegap game server deployments on demand, placing players into low-latency sessions at the optimal location based on their network data.

The guide has four parts:

  1. Prepare the Unity server: install the plugins your headless server needs to talk to Nakama, and upload the server to Edgegap.
  2. Set up Nakama: add the fleet manager plugin and configure it with your Edgegap details.
  3. Connect the two: store player locations so Edgegap can pick the closest server, and have your game server report its status back to Nakama.
  4. Request servers: trigger deployments from the matchmaker, or from client RPCs.

Before you begin #

Nakama Version
The Nakama-Edgegap plugin has been updated to support Nakama 3.39.

Before proceeding make sure that you have:

Prepare the Unity server #

Nakama can only manage a game server if the server tells Nakama when it’s ready, who’s connected, and when it’s stopping. This part adds the Unity plugin that does this for you, and uploads your server build to Edgegap so Nakama has something to deploy.

This guide doesn’t cover synchronizing gameplay state between the headless server and connected clients. Once Edgegap places players into a session, that connection is direct and handled by your own networking framework (for example Unity Netcode for GameObjects, Mirror, or Photon Fusion).

Install the Nakama SDK in Unity #

Install the Nakama SDK so your Unity project can communicate with Nakama.

  1. Download the latest Nakama Unity SDK package.
  2. Double-click the Nakama.unitypackage and install it in your Unity project.

The Nakama SDK is now available in your Unity project.

Install the Edgegap Server Nakama plugin #

Install the Edgegap Server Nakama plugin so your game server can report its status and connected players to Nakama.

  1. Open your Unity project.
  2. Select Window > Package Manager.
  3. Click the + icon and select Add package from git URL….
  4. Enter https://github.com/edgegap/edgegap-server-nakama-plugin-unity.git.
  5. Click Add and wait for the Unity Package Manager to complete the installation.

The Edgegap Server Nakama Plugin package now appears in the Package Manager.

Import the simple example #

The plugin includes a sample handler that shows the minimum code needed to report to Nakama. Import it to use as a starting point for your own server code.

  1. Find the Edgegap Server Nakama Plugin package in the Package Manager window.
  2. Open the Samples tab.
  3. Click Import next to Simple Handler Example.
  4. Find the sample files in Assets/Samples/Edgegap Server Nakama Plugin/{version}/Simple Handler Example.
  5. Create an empty GameObject in your scene and attach the SimpleHandlerExample.cs script to it.

The sample creates the plugin’s ServerAgent, tells Nakama the server is ready when the script starts, sends queued connection updates on every fixed update, and stops the instance when the application quits.

The sample can’t know when players connect or disconnect, because that depends on your multiplayer framework. Call the script’s AddUser and RemoveUser methods from your framework’s connection and disconnection callbacks so Nakama can track seat allocations. See Report game server lifecycle events to Nakama for what each call does.

Upload your server to Edgegap #

Edgegap needs a build of your server before Nakama can deploy it.

  1. Go to Tools > Edgegap Hosting.
  2. Log in and follow steps 1 to 5 in the tool.

Skip step 6. It deploys a server manually, which isn’t needed since Nakama handles deployments for you.

Edgegap Hosting Unity Tool panel showing the login screen and the five-step deployment setup workflow
Edgegap Hosting Unity Tool

Your server is now uploaded to Edgegap. Make a note of the application name, version name, and port name you used. You need them when you configure Nakama.

Troubleshooting #

Visual Studio shows type or namespace name could not be found for Edgegap namespace.

  1. In your Unity Editor, navigate to Edit > Preferences > External Tools > Generate .csproj files.
  2. Make sure you’ve enabled Git packages.
  3. Click Regenerate project files.

Install the Nakama-Edgegap plugin #

The Nakama-Edgegap plugin is a Go package that implements Nakama’s FleetManager interface. It creates, tracks, and tears down Edgegap deployments from within your Nakama server, so the matchmaker and your own code can request game servers without calling Edgegap directly. To add it to your Nakama Go runtime project, run the following command:

1
go get github.com/edgegap/nakama-edgegap

The plugin is now listed in your project’s go.mod dependencies.

Configure Nakama #

Nakama needs credentials to call Edgegap’s API, and the names of the application, version, and port you created in Edgegap, so it knows what to deploy. Set the following environment variables in your Nakama cluster:

1
2
3
4
5
6
EDGEGAP_API_URL=https://api.edgegap.com
EDGEGAP_API_TOKEN=<The Edgegap's API Token (keep the 'token' in the API Token)>
EDGEGAP_APPLICATION=<The Edgegap's Application Name to use to deploy>
INITIAL_EDGEGAP_VERSION=<Initial version to use if none exists in storage>
EDGEGAP_PORT_NAME=<The Edgegap's Application Port Name to send to game client>
NAKAMA_ACCESS_URL=<Nakama API Url, for Heroic Cloud, it will be provided when you create your instance>

NAKAMA_ACCESS_URL must be prefixed with https://. The plugin combines it with your Nakama runtime.http_key to build the callback URLs that your game server and Edgegap’s deployment webhooks use to report back to Nakama. Make sure the URL is reachable from the internet and that you’ve set a non-default HTTP key.

To run a local cluster, copy local.yml.example from the plugin repository to local.yml and fill it in.

You can also set the following optional variables:

1
2
3
EDGEGAP_POLLING_INTERVAL=<Interval where Nakama will sync with Edgegap API in case of mismatch (default:15m)>
NAKAMA_CLEANUP_INTERVAL=<Interval where Nakama will check reservations expiration (default:1m)>
NAKAMA_RESERVATION_MAX_DURATION=<Max Duration of a reservations before it expires (default:30s)>

Setting EDGEGAP_POLLING_INTERVAL or NAKAMA_CLEANUP_INTERVAL to an empty value or 0 disables that background worker. This is useful for testing but isn’t recommended in production.

How the plugin tracks available seats #

The plugin stores each Edgegap deployment as a Nakama instance, and keeps two lists in the instance’s metadata: seat reservations for players who’ve been assigned to the server but haven’t connected yet, and active connections reported by the game server. It uses them to calculate available_seats:

1
max_players - reservations - connections = available_seats

Reservations that aren’t claimed within NAKAMA_RESERVATION_MAX_DURATION expire, which frees the seat. You can query available_seats when you list instances.

Register the fleet manager #

Registering the fleet manager tells Nakama to use Edgegap for all fleet manager calls, and registers the RPCs and webhooks Edgegap and your game server call back to. In your main.go, add the following to your InitModule function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Register the Fleet Manager
efm, err := fleetmanager.NewEdgegapFleetManager(ctx, logger, db, nk, initializer)
if err != nil {
    return err
}

if err = initializer.RegisterFleetManager(efm); err != nil {
    logger.WithField("error", err).Error("failed to register Edgegap fleet manager")
    return err
}

To test locally, copy docker-compose.yml and Dockerfile from the plugin repository to the root of your project, and run the following command to start a local cluster:

1
docker compose up --build -d

Nakama starts with the Edgegap fleet manager registered. Check the Nakama logs for errors that mention missing environment variables. Run the following command to stop the cluster:

1
docker compose down

Store player IP addresses for server placement #

Edgegap’s server score strategy picks the best data center using the public IP addresses of the players in a session. Game clients never call Edgegap directly. They go through Nakama, so Nakama must capture each player’s IP when they authenticate and keep it in their account metadata as PlayerIp.

The plugin provides an OnAuthenticateUpdate* hook for each authentication method. In your InitModule function, register the hook that matches the authentication method your game uses. For example, for custom authentication:

1
2
3
4
// Register Authentication Methods
if err := initializer.RegisterAfterAuthenticateCustom(fleetmanager.OnAuthenticateUpdateCustom); err != nil {
    return err
}

After each successful authentication, Nakama now stores the player’s public IP in their account metadata as PlayerIp. When a session is created, the plugin sends these IP addresses to Edgegap.

Report game server lifecycle events to Nakama #

Nakama can’t see inside your game server, so it relies on the server to report what’s happening. Without these reports, Nakama never tells waiting players that their server is ready, and it can’t tell how many seats are free or when to tear the deployment down.

Your dedicated game server must report two kinds of lifecycle events back to Nakama:

  • Connection events: which players are currently connected. Nakama uses these to update available seats.
  • Instance events: the server’s own status (ready, in error, or stopped). Nakama uses these to notify players and to terminate the deployment.

When Nakama creates a deployment, it passes the URLs for both events to Edgegap, which injects them into the game server’s environment as environment variables.

Use the Unity plugin #

The Edgegap Server Nakama Plugin for Unity handles the entire reporting protocol for you: reading the injected environment variables, building the request payloads, and sending them to Nakama with retries. If you started from the Simple Handler Example, most of these calls are already in place. To use the plugin in your own code:

  • Call Initialize() when your server is ready to accept players.
  • Call AddUser(userID) and RemoveUser(userID) from your networking framework’s connect and disconnect callbacks.
  • Call FixedUpdate() on every fixed update, so queued connection changes get sent.
  • Call StopInstance(message, err) when your server shuts down. This sends a STOP event, which tells Edgegap to terminate the deployment. If err is set, it sends an ERROR event instead.

The plugin batches connection updates every second by default. Pass a different value to connectionUpdateFrequencySeconds in the ServerAgent constructor to change this.

Once these calls are in place, Nakama marks your instance as ready when the server starts, keeps its seat count up to date as players join and leave, and terminates the deployment when the server stops.

Report events without the Unity plugin #

If you’re using a different engine or language, send these two HTTP requests directly to the URLs Nakama provides. Both calls are plain HTTP POST requests with a JSON body and a Content-Type: application/json header. No additional authentication is required, because the URLs already include the credentials Nakama needs.

Environment variables #

The following environment variables are available in the dedicated game server when it’s created through Nakama:

  • NAKAMA_CONNECTION_EVENT_URL: the URL for sending player connection events
  • NAKAMA_INSTANCE_EVENT_URL: the URL for sending instance status events
  • NAKAMA_INSTANCE_METADATA: a JSON object containing the instance’s creation metadata

Send connection events #

POST to NAKAMA_CONNECTION_EVENT_URL with the full list of currently connected players:

1
2
3
4
{
  "instance_id": "<instance_id>",
  "connections": ["<user_id>"]
}

Each request overwrites the existing connection list for the instance, so send the complete list of connected user IDs, not just what changed. Collect connection changes for a few seconds and send them as a single batch, rather than one request per connect or disconnect.

Send instance events #

POST to NAKAMA_INSTANCE_EVENT_URL to report your server’s status:

1
2
3
4
5
6
{
  "instance_id": "<instance_id>",
  "action": "READY",
  "message": "",
  "metadata": {}
}

action accepts one of three values:

  • READY: marks the instance ready and triggers a Nakama callback that notifies waiting players.
  • ERROR: marks the instance as failed. Use message to describe what went wrong.
  • STOP: calls Edgegap’s API to terminate the deployment. Nakama removes the instance once Edgegap confirms termination.

Use metadata to merge custom key-value data into the instance’s metadata on Nakama. You can then use this data in instance queries.

Your game server is now reporting its lifecycle to Nakama.

Request servers with client RPCs (optional) #

The plugin registers a set of RPCs that let game clients list, create, get, and join instances directly. Use them to prototype quickly or to build a server browser. For production use cases, use the matchmaker instead. It gives you more control over who can start servers, and it adds security and flexibility.

Create an instance #

Call the instance_create RPC to deploy a new game server for one or more players:

1
2
3
4
5
{
  "max_players": 2,
  "user_ids": [],
  "metadata": {}
}

Set max_players to -1 for unlimited players. Use this with caution, and benchmark your server’s resource usage first.

If user_ids is empty, Nakama uses the requesting user’s ID. Each listed user gets a seat reservation, and Nakama uses their stored PlayerIp values to choose the deployment location.

The RPC returns as soon as the deployment request is accepted, not when the server is ready. Its response includes the deployment_id. When the server reports READY, Nakama sends each user a notification with the connection details:

  • Code 111: the server is ready. The notification content contains IpAddress, DnsName, Port, and InstanceId.
  • Code 112: creating the server timed out.
  • Code 113: creating the server failed.

Your client connects to the game server using these details, so listen for these notifications after calling the RPC.

Get an instance #

Call the instance_get RPC to retrieve the details of an instance, including its status and metadata:

1
2
3
{
  "instance_id": "<instance_id>"
}

List instances #

Call the instance_list RPC to find instances. Use query to filter results, for example to find instances with free seats:

1
2
3
4
5
{
  "query": "",
  "limit": 100,
  "cursor": ""
}

The following example lists all READY instances with at least one available seat:

1
2
3
4
5
{
  "query": "+value.metadata.edgegap.available_seats:>=1 +value.status:READY",
  "limit": 100,
  "cursor": ""
}

Join an instance #

Call the instance_join RPC to reserve seats on an existing instance:

1
2
3
4
{
  "instance_id": "<instance_id>",
  "user_ids": []
}

If user_ids is empty, Nakama uses the requesting user’s ID. The response contains the information your client needs to connect to the game server. The reservation expires if the player doesn’t connect within NAKAMA_RESERVATION_MAX_DURATION.

Request servers with the matchmaker #

Using the matchmaker is the recommended way to start game servers in production. When the matchmaker finds a match, your server code, not the client, decides to create the server. This stops clients from deploying servers on demand and gives you one place to apply your own rules.

The following sample uses the OnMatchmakerMatched hook to deploy an Edgegap server for the matched players, then sends each player a notification with the connection details:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// OnMatchmakerMatched When a match is created via matchmaker, collect the Users and create a instance
func OnMatchmakerMatched(ctx context.Context, logger runtime.Logger, db *sql.DB, nk runtime.NakamaModule, entries []runtime.MatchmakerEntry) (string, error) {
	maxPlayers := len(entries)
	userIds := make([]string, 0, len(entries))
	for _, entry := range entries {
		userIds = append(userIds, entry.GetPresence().GetUserId())
	}

	var callback runtime.FmCreateCallbackFn = func(status runtime.FmCreateStatus, instanceInfo *runtime.InstanceInfo, sessionInfo []*runtime.SessionInfo, metadata map[string]any, createErr error) {
		switch status {
		case runtime.CreateSuccess:
			logger.Info("Edgegap instance created: %s", instanceInfo.Id)

			content := map[string]interface{}{
				"IpAddress":  instanceInfo.ConnectionInfo.IpAddress,
				"DnsName":    instanceInfo.ConnectionInfo.DnsName,
				"Port":       instanceInfo.ConnectionInfo.Port,
				"InstanceId": instanceInfo.Id,
			}
			// Send connection details notifications to players
			for _, userId := range userIds {
				subject := "connection-info"

				code := notificationConnectionInfo
				err := nk.NotificationSend(ctx, userId, subject, content, code, "", false)
				if err != nil {
					logger.WithField("error", err.Error()).Error("Failed to send notification")
				}
			}
			return
		case runtime.CreateTimeout:
			logger.WithField("error", createErr.Error()).Error("Failed to create Edgegap instance, timed out")

			// Send notification to client that instance session creation timed out
			for _, userId := range userIds {
				subject := "create-timeout"
				content := map[string]interface{}{}
				code := notificationCreateTimeout
				err := nk.NotificationSend(ctx, userId, subject, content, code, "", false)
				if err != nil {
					logger.WithField("error", err.Error()).Error("Failed to send notification")
				}
			}
		default:
			logger.WithField("error", createErr.Error()).Error("Failed to create Edgegap instance")

			// Send notification to client that instance session couldn't be created
			for _, userId := range userIds {
				subject := "create-failed"
				content := map[string]interface{}{}
				code := notificationCreateFailed
				err := nk.NotificationSend(ctx, userId, subject, content, code, "", false)
				if err != nil {
					logger.WithField("error", err.Error()).Error("Failed to send notification")
				}
			}
			return
		}
	}

	efm := nk.GetFleetManager()
	_, err := efm.Create(ctx, maxPlayers, userIds, nil, nil, callback)

	if err != nil {
		logger.WithField("error", err.Error()).Error("failed to start creating fleet manager instance")
		return "", ErrInternalError
	}

	reply := instanceCreateReply{
		Message: "Instance Created",
		Ok:      true,
	}

	replyString, err := json.Marshal(reply)
	if err != nil {
		logger.WithField("error", err.Error()).Error("failed to marshal instance create reply")
		return "", ErrInternalError
	}

	return string(replyString), err
}

The sample refers to notificationConnectionInfo, notificationCreateTimeout, notificationCreateFailed, and instanceCreateReply, which you need to define in your own module. The plugin’s client RPCs use notification codes 111, 112, and 113 for these, and you can reuse them so clients handle both flows the same way.

Register the OnMatchmakerMatched callback in the InitModule function in your main.go:

1
2
3
4
5
err = initializer.RegisterMatchmakerMatched(OnMatchmakerMatched)
if err != nil {
    logger.WithField("error", err).Error("failed to register Matchmaker matched with fleet manager")
    return err
}

When players are matched, Nakama now deploys an Edgegap server near them, waits for your game server to report READY, and sends the connection details to each player as a notification. When the server stops, Nakama terminates the deployment.

Additional resources #