Skip to main content

How to host a Nakama server for $10/mo

Submitted by David Snopek on Monday, 2021-07-26 @ 8:13am
Hosting Nakama

Nakama is a scalable Open Source game backend, that I've written a couple articles about recently.

I encountered an interesting response to my posts that I wasn't expecting:

"Nakama is too expensive"

However, I have a small indie game (Retro Tank Party) for sale on Steam that's running Nakama on a $10/mo Linode!

Of course, running a single, low-powered server won't work for all games. Some might need multiple servers (especially for high availability if one goes down) or a more expensive, higher-powered server.

But for testing in early development - OR if your game is small enough and you can tolerate some downtime if the data center is hit by a tornado - a single $10/mo server from Linode, or Digital Ocean, or wherever is totally sufficient.

In this tutorial, I'll show you how to setup Nakama with production configuration, and explain how to perform some routine maintenance on it.

Wait - why do people think Nakama is expensive?

Since I went with the click-bait title and intro about price (sorry!), I figure I should quickly explain why this misconception exists.

Heroic Labs, the creators of Nakama, sell their own hosting for Nakama called the Heroic Cloud. It costs $600/mo for the lowest plan.

They're clearly targeting bigger games that are generating enough revenue month over month to justify that cost.

But that's the beauty of Open Source!

You don't have to host on the Heroic Cloud - you can host anywhere you want.

Prerequisites

First of all, I'm going to assume that you have basic Linux skills, can navigate your way around on the command-line, and install new software. If not, you may need to find a friend to help you set this up. :-)

To get started, launch a new server using whichever Linux distribution you are most comfortable with (I'm using Ubuntu) on the hosting provider of your choice (I'm using Linode).

Then install Docker and Docker Compose. Those links will take you to the installation documentation, which is pretty good, but I'm sure you can find tutorials online.

Also, it probably goes without saying that you should do some basic security setup on your server, for example:

  1. Harden your SSH configuration (ex. disable root login, require key-based authentication, etc)
  2. Enable automatic security updates from your distro
  3. Uninstall any internet-facing software that you don't need

But that's just basic Linux stuff that I'm not going to cover here.

Create the Docker Compose app

We're going to make a Docker Compose app to run Nakama and the related services.

First, you'll need to create a directory where it's going to live. On my servers, I like to use a top-level directory called /app to hold my Docker Compose apps:

sudo mkdir -p /app/nakama
cd /app/nakama

Then we need to create a docker-compose.yml file with the following contents:

version: '3'

services:
  cockroachdb:
    image: cockroachdb/cockroach:v19.2.5
    command: start --insecure --store=attrs=ssd,path=/var/lib/cockroach/
    restart: always
    volumes:
      - ./db:/var/lib/cockroach
    expose:
      - "8080"
      - "26257"
  nakama:
    image: heroiclabs/nakama:3.4.0
    entrypoint:
      - "/bin/sh"
      - "-ecx"
      - > 
          /nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
          exec /nakama/nakama --config /nakama/data/config.yml --database.address root@cockroachdb:26257
    restart: always
    links:
      - "cockroachdb:db"
    depends_on:
      - cockroachdb
    volumes:
      - ./data:/nakama/data
    expose:
      - "7349"
      - "7350"
      - "7351"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:7350/"]
      interval: 10s
      timeout: 5s
      retries: 5
  nginx:
    image: jonasal/nginx-certbot:latest
    environment:
      CERTBOT_EMAIL: "[email protected]"
    ports:
      - "80:80"
      - "443:443"
      - "7349:7349"
      - "7350:7350"
      - "7351:7351"
    volumes:
      - nginx_secrets:/etc/letsencrypt
      - ./nginx_conf.d:/etc/nginx/user_conf.d
    links:
      - nakama
    depends_on:
      - nakama

volumes:
  nginx_secrets: {}

This creates 3 services:

  • CockroachDB: This is the database backend used by Nakama. (FYI, if you're more comfortable with Postgres, it can be used with Nakama instead.)
  • Nakama: Specifically, version 3.4.0 which is the latest at the time of this writing. We'll talk about how to upgrade Nakama later in the article.
  • NGINX: We're using NGINX as a reverse-proxy in front of Nakama, so we can provide access via secure HTTPS, rather than HTTP.

Be sure to replace the "[email protected]" in your docker-compose.yml with a real email address! This is required by Let's Encrypt to generate an SSL certificate for HTTPS.

And before we move on, we need to make sure to create a few directories:

# The raw data from CockroachDB for easier backups.
sudo mkdir db
 
# The Nakama data directory, for configuration and custom Nakama modules.
sudo mkdir data
sudo mkdir data/modules
 
# The NGINX configuration.
sudo mkdir nginx_conf.d

Configure Nakama

Next, we'll be configuring Nakama for production use, which requires setting a bunch of secret keys.

You can use any string of characters, but I'd recommend randomly generating them, and using at least 32 characters. If you don't have any better options (I like to use LastPass), you can use Random.org to generate them.

Create a file called config.yml inside the 'data' directory we just created above:

name: nakama1
socket:
  # Replace this with a random string.
  server_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

session:
  # 6h token expiry
  token_expiry_sec: 21600
  # Replace both of these with a random strings.
  encryption_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  refresh_encryption_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

runtime:
  # Replace this with a random string.
  http_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

console:
  # Replace these with a secure username and password.
  username: "adminuser"
  password: "adminpass"
 
  # Replace this with a random string.
  signing_key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

If you have any Nakama modules (or create any later), you'll want to copy those into the 'data/modules' directory.

Configure NGINX

We need to tell NGINX the domain name of your server, and to proxy traffic to Nakama.

Create a file called nakama.conf in the 'nginx_conf.d' directory we created earlier:

# Normal web traffic.
server {
  # Listen to port 443 on both IPv4 and IPv6.
  listen 443 ssl default_server reuseport;
  listen [::]:443 ssl default_server reuseport;
 
  # Domain names this server should respond to.
  server_name nakama.example.com;
 
  # SSL certificate configuration.
  ssl_certificate         /etc/letsencrypt/live/nakama/fullchain.pem;
  ssl_certificate_key     /etc/letsencrypt/live/nakama/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/nakama/chain.pem;
  ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;
 
  location / {
    # Redirect web visitors to your normal website.
    return 301 https://www.example.com$request_uri;
  }
}
 
# Nakama gRPC API.
server {
  listen 7349 ssl;
  listen [::]:7349 ssl;
 
  # Domain names this server should respond to.
  server_name nakama.example.com;
 
  # SSL certificate configuration.
  ssl_certificate         /etc/letsencrypt/live/nakama/fullchain.pem;
  ssl_certificate_key     /etc/letsencrypt/live/nakama/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/nakama/chain.pem;
  ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;
 
  location / {
    proxy_pass http://nakama:7349;
  }
}
 
# Nakama HTTP API.
server {
  listen 7350 ssl;
  listen [::]:7350 ssl;
 
  # Domain names this server should respond to.
  server_name nakama.example.com;
 
  # SSL certificate configuration.
  ssl_certificate         /etc/letsencrypt/live/nakama/fullchain.pem;
  ssl_certificate_key     /etc/letsencrypt/live/nakama/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/nakama/chain.pem;
  ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;
 
  location / {
    # Enable CORS from anywhere with support for pre-flight requests.
    # See: https://enable-cors.org/server_nginx.html
    if ($request_method = 'OPTIONS') {
       add_header 'Access-Control-Allow-Origin' '*';
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
       # Tell client that this pre-flight info is valid for 20 days
       add_header 'Access-Control-Max-Age' 1728000;
       add_header 'Content-Type' 'text/plain; charset=utf-8';
       add_header 'Content-Length' 0;
       return 204;
    }
    if ($request_method = 'POST') {
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
       add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
    if ($request_method = 'GET') {
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
       add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
 
    proxy_pass http://nakama:7350;
  }
 
  location /ws {
    # Enable CORS from anywhere with support for pre-flight requests.
    # See: https://enable-cors.org/server_nginx.html
    if ($request_method = 'OPTIONS') {
       add_header 'Access-Control-Allow-Origin' '*';
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
       # Tell client that this pre-flight info is valid for 20 days
       add_header 'Access-Control-Max-Age' 1728000;
       add_header 'Content-Type' 'text/plain; charset=utf-8';
       add_header 'Content-Length' 0;
       return 204;
    }
    if ($request_method = 'POST') {
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
       add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
    if ($request_method = 'GET') {
       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
       add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
       add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
    }
 
    proxy_pass http://nakama:7350/ws;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
  }
}
 
# Nakama admin console.
server {
  listen 7351 ssl;
  listen [::]:7351 ssl;
 
  # Domain names this server should respond to.
  server_name nakama.example.com;
 
  # SSL certificate configuration.
  ssl_certificate         /etc/letsencrypt/live/nakama/fullchain.pem;
  ssl_certificate_key     /etc/letsencrypt/live/nakama/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/nakama/chain.pem;
  ssl_dhparam /etc/letsencrypt/dhparams/dhparam.pem;
 
  location / {
    proxy_pass http://nakama:7351;
  }
}

That's a lot of stuff! Luckily, you don't need to understand most of it. :-)

You just need to replace a couple of domain names:

  • Replace nakama.example.com with the domain name of your Nakama server.
  • Replace www.example.com with your normal website. If anyone tries to visit your Nakama server in their web browser, they'll be redirected there.

Start it all up!

In the directory with the docker-compose.yml (which would be /app/nakama if you're following my convention) run:

sudo docker-compose up -d

The first time you run this it could take awhile.

It needs to download the Docker images for CockroachDB, Nakama and NGINX. Then the first time each of those run, they need to do some first time setup. NGINX will probably be the slowest, because it needs to get an SSL certificate from Let's Encrypt.

You can check on what NGINX is doing by running:

sudo docker-compose logs -f nginx

Anyway, if everything has gone well, you should be able to point your Nakama client at this server! Be sure to use the random 'server_key' you put in Nakama's config.yml earlier.

Basic maintenance

All servers need on-going maintenance. The two most important things are: backups, and updates.

Backing up Nakama with the configuration we used here is super easy! You just need to make an archive of everything in the same directory as the docker-compose.yml (ie. /app/nakama) and all its sub-directories. This will get the full database and Nakama's configuration. You can restore your Nakama instance on a new server by simply extracting the backup, and running 'docker-compose up -d'.

I'd recommend setting up an automatic daily off-site backup, as well as performing a quick manual backup (ie. copying the directory :-)) just before updating Nakama or CockroachDB.

To update Nakama or CochroachDB, simply edit the 'docker-compose.yml' file, change the version number after the 'image' name (ie. change 'nakama:3.4.0' to 'nakama:3.5.0', for example) and run:

sudo docker-compose pull
sudo docker-compose stop
sudo docker-compose up -d

NOTE: This will take your Nakama instance down during the update! So, you'll need to schedule it for a slow time and let your players know. 

That's about it!

I know that might seem like a lot, but once you get it going, there's not much to do but periodic updates.

Please let know how it goes for you!

Happy Hacking :-)

    Level:
    Advanced
    Topic:

    Subscribe!

    Subscribe to get an email when new articles or videos are posted on SnopekGames.com!

    * indicates required

    Comments

    Submitted by wooz on Sunday, 2021-10-17 @ 9:38am Permalink

    Hi, thank you for this tutorial!
    I've got just a little problem, from nginx I get this error when try to reach the admin console and I don't know the reason:

    400 Bad Request The plain HTTP request was sent to HTTPS port

    Another thing, you should point out that for every http://nakama entry it's necessary to enter the server hostname or something like localhost/127.0.0.1

    Thanks again

    Bye

    Submitted by David Snopek on Monday, 2021-10-18 @ 10:04am Permalink

    Thanks!

    Another thing, you should point out that for every http://nakama entry it's necessary to enter the server hostname or something like localhost/127.0.0.1

    Hm. It's not necessary to change those, and, in fact, if you change them to localhost or 127.0.0.1, it shouldn't work.

    Nakama and NGINX are in separate containers, which you can think of as separate computers, so if the NGINX config uses http://127.0.0.1 it'll try to proxy to itself rather than to the Nakama container. Docker should automatically add entries to the /etc/hosts file for each of the linked containers, so having http://nakama in the NGINX config is correct!

    400 Bad Request The plain HTTP request was sent to HTTPS port

    Perhaps this is related to the above?

    Nakama should be serving the admin console as HTTP, since we didn't configure any SSL in Nakama itself, so there's no way it should be returning HTTPS data.

    I also just double checked my production server, and the admin console is working, and the config matches what's in the tutorial.

    Anyway, I hope that helps!

    Submitted by wooz on Tuesday, 2021-11-02 @ 5:06pm Permalink

    Hi, sorry for the late response.
    Yeah you were right, it was my fault, I changed it back and now it's working, I'm a little new with docker and I did overthink it a bit.
    Thank you again for your help and for this tutorial!

    Submitted by Rokas on Friday, 2021-11-19 @ 11:19am Permalink

    Hey,

    I've stumbled onto Nakama because of your other tutorial, which let me set up basic connections relatively easily in windows, linux and android. However, when it came to the html build, I ran face first into a wall.

    Things I've figured out:
    - html builds, if hosted on ssl secured connections (https) cannont make http request to http pages
    - by default, nakama uses http, meaning that hosting the game on say itchio will disable the multiplayer aspect
    - to fix this you need to have your webserver to proxy (?) the connections from https to http, which this specific tutorial seems to do

    However, no matter what I do, I cannot have both (1) have the connections be served through https and (2) be able to view the nakama console and connect to the service from the game.

    Specifically, I've tried following the steps you've outlined here one by one, ending up with a state where, from what I understand, nginx is spun up by docker-compose, which then maps the used ports (7350,7351...) from externally accessed https to the nakama-visible http. Seemingly done securing the connection, I can no longer access the nakama console through http(s)://IP/7351, which now yields "This site can’t be reached" (Accessing it the same way works fine when nakama is not behind nginx through docker-compose, but then the game is not playable using the html build).

    I hope that there is a simple step I've missed somewhere that causes this setup to not work.
    Can you think of what it could be?

    Thanks

    Submitted by David Snopek on Friday, 2021-11-19 @ 2:09pm Permalink

    I can no longer access the nakama console through http(s)://IP/7351, which now yields "This site can’t be reached" (Accessing it the same way works fine when nakama is not behind nginx through docker-compose, but then the game is not playable using the html build).

    Hm. You should still be able to access the Nakama console - I'm using the configuration described in this blog post for my live Nakama server and I can access the console just fine!

    One thing in your comment has me wondering - you wrote "http(s)://IP:7351" where I'm assuming IP is a stand-in for your server's IP address. Generally, HTTPS is used with domain names, and not IP addresses. While it's technically possible to use an IP, it's less compatible and some applications may have problems accessing it. Also, where "http(s)" would imply either http or https, you can only use https to access port 7351 (or any of the other ports) because that's what NGINX is configured to serve.

    But if that's not the problem, I'd recommend double checking the contents of your "nginx_conf.d/nakama.conf" file, particularly that you replaced all instances of "nakama.example.com" with your real domain name (it's in there 4 times), especially the last one because that's for the Nakama console.

    Also, it may be worth looking at the NGINX logs, which you can get by running:

    docker-compose logs nginx

    ... in the same directory as the docker-compose.yml. This may give some clue as to why it's not working.

    Hopefully, that helps! If you want to try and debug it in realtime, feel free to drop by my Discord (link in the social links in the footer).

    Submitted by Rokas on Saturday, 2021-11-20 @ 5:53am Permalink

    Thanks for the reply and pointing me in the right direction.
    I went through the steps one more time from scratch and have been able to resolve the problems for the time being.

    The pitfalls I fell into:
    - I initially replaced "nakama.example.com" with "nakama.site.com", where it should have been "site.com"
    - I tried accessing the console through the browser cache'd version of "site.com:7351", which was using http and thus failing, manually adding https:// fixed this
    - the changes above made the nakama server accessible with https, however my static web pages were now 404'ing. My guess was that nginx, when run from the docker-compose environment (as opposed to by itself, like I was doing it before), couldn't find the local root of the index.html files. That was fixed by adding "- /var/www:/var/www" line to the volumes section of docker-compose.yml

    Thanks again, HTML5 builds successfully connect now!

    Submitted by David Snopek on Monday, 2021-11-22 @ 8:59am Permalink

    Awesome, glad you got it working!

    the changes above made the nakama server accessible with https, however my static web pages were now 404'ing. My guess was that nginx, when run from the docker-compose environment (as opposed to by itself, like I was doing it before)

    Ah, so you were previously using this server to serve static HTML pages? The tutorial is written based on the assumption that you're launching a new blank server for Nakama, and that it's only going to be doing Nakama.

    What you added is great! But since you're serving normal web content, you should probably also configure nginx to listen on port 80 (even if all you're doing is redirecting to https). You'd need to add something like this (but I didn't actually test it, so it may not be exactly right):

    server {
      listen 80;
      listen [::]:80;
     
      server_name nakama.example.com;
     
      location / {
        return 301 https://nakama.example.com$request_uri;
      }
    }

    And I assume you've probably already removed the other redirect, since you said your content is being served correctly.

    Best of luck with your game! :-)

    Submitted by Nicolas Vergara on Sunday, 2021-12-05 @ 9:51am Permalink

    You are amazing!!!! Thank you so much!! this was very helpful for me. Nakama is great but sometimes its a headacke.
    You are great!

    Submitted by Thanh Vu on Thursday, 2022-06-30 @ 11:44pm Permalink

    Hi, thanks for share your knowledge. I'm so grateful for that.
    I followed the instructions and got some problems. When I run docker-compose up I have seen some errors like this :
    - nginx_1 | Certbot failed for 'nakama'. Check the logs for details.
    nginx_1 | Could not find keyfile file '/etc/letsencrypt/live/nakama/privkey.pem' in '/etc/nginx/conf.d/nakama.conf.nokey'
    nginx_1 | Could not find fullchain file '/etc/letsencrypt/live/nakama/fullchain.pem' in '/etc/nginx/conf.d/nakama.conf.nokey'
    nginx_1 | Could not find chain file '/etc/letsencrypt/live/nakama/chain.pem' in '/etc/nginx/conf.d/nakama.conf.nokey'
    What should I do next ?

    Submitted by David Snopek on Wednesday, 2022-07-06 @ 12:05pm Permalink

    Thanks!

    Well, generally speaking, it seems as though Let's Encrypt isn't creating the HTTPS certificate for you. However, there is one weird thing: you have a configuration file called "nakama.conf.nokey"? The tutorial has you creating only a "nakama.conf" file. You shouldn't have extra files in the "nginx_conf.d" directory, because they'll all get loaded up by NGINX.

    To see if there's any error messages from Let's Encrypt, you can look at the NGINX logs by running:

    docker-compose logs nginx

    Or, check out the Let's Encrypt debug logs (these are super technical) via:

    docker-compose exec nginx cat /var/log/letsencrypt/letsencrypt.log

    I hope that helps!

    Submitted by James on Thursday, 2022-08-04 @ 7:42pm Permalink

    Hi David,
    First of all thank you for creating the Godot plugins for connecting to Nakama, and the tutorials and contributions to the Godot engine. I attempted to following along on this tutorial, but when unsuccessful, started trying to piece different things together from your tutorial and Nakama's docker compose which I had previously working with a tutorial I followed on Mitch's channel FinePointCGI https://www.youtube.com/watch?v=D-lgqp9USlY. Anyway My current setup has all the project code he uses the docker-compose.yml from Nakama's site and your addition of the nginx reverse proxy docker. everything in godot works over https and the direct peer to peer connection, but when exporting to the web I get a console error: connection has failed Device ID is required. Any ideas on what I might have misconfigured?

    Submitted by Pete on Monday, 2023-05-29 @ 12:23am Permalink

    Is it okay to launch the cockroachdb container with --insecure ? It displays the following scary message but hopefully the cockroachdb container isn't accessible from the outside world ?

    nakama-cockroachdb-1 | *
    nakama-cockroachdb-1 | * WARNING: ALL SECURITY CONTROLS HAVE BEEN DISABLED!
    nakama-cockroachdb-1 | *
    nakama-cockroachdb-1 | * This mode is intended for non-production testing only.
    nakama-cockroachdb-1 | *
    nakama-cockroachdb-1 | * In this mode:
    nakama-cockroachdb-1 | * - Your cluster is open to any client that can access any of your IP addresses.
    nakama-cockroachdb-1 | * - Intruders with access to your machine or network can observe client-server traffic.
    nakama-cockroachdb-1 | * - Intruders can log in without password and read or write any data in the cluster.
    nakama-cockroachdb-1 | * - Intruders can consume all your server's resources and cause unavailability.
    nakama-cockroachdb-1 | *
    nakama-cockroachdb-1 | *
    nakama-cockroachdb-1 | * INFO: To start a secure server without mandating TLS for clients,
    nakama-cockroachdb-1 | * consider --accept-sql-without-tls instead. For other options, see:
    nakama-cockroachdb-1 | *
    nakama-cockroachdb-1 | * - https://go.crdb.dev/issue-v/53404/v20.2
    nakama-cockroachdb-1 | * - https://www.cockroachlabs.com/docs/v20.2/secure-a-cluster.html

    Add new comment
    The content of this field is kept private and will not be shown publicly.

    Plain text

    • No HTML tags allowed.
    • Lines and paragraphs break automatically.
    • Web page addresses and email addresses turn into links automatically.
    CAPTCHA
    This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.