DEV Community

Cover image for Hosting a Minecraft Server on Cloudflare Containers
Sam Gunawardana
Sam Gunawardana

Posted on

Hosting a Minecraft Server on Cloudflare Containers

Introduction

This project runs a Minecraft server on stateless Cloudflare Containers.

But Why...

I got the idea for this project while playing on a friend’s Minecraft server that often struggled with performance issues. The server was hosted on my friend's laptop and connected through playit.gg since his internet connection didn’t support port forwarding. I started thinking about more reliable ways to host a Minecraft server. I would've hosted on my Raspberry Pi like I have done in the past for servers with friends, but this one had mostly overseas players. Traditional game server hosts can be pricey, and cloud providers like AWS aren’t cheap either, especially since Minecraft servers need plenty of RAM and solid CPU performance to handle map generation and chunk loading well.

That’s when I remembered that Cloudflare launched their Containers service earlier this year. As a Cloudflare customer, I already had access to it, so I figured, why not give it a try? It seemed like the perfect opportunity to experiment, expand my knowledge about containerised environments, and see if I could build a more affordable Minecraft hosting setup.

Goals

  • Run the Minecraft server
  • View the server console and send commands to it

Terms

  • Cloudflare Workers: Development platform for serverless applications
  • Cloudflare Durable Objects: A combined storage and compute product
  • Cloudflare Containers: Containers controlled by a Durable Object.

Limitations and Issues

Before beginning the project, I took time to read the documentation for Cloudflare Containers. I am already very familiar with Cloudflare Workers and Durable Objects, which are needed to run and manage the containers, so this knowledge was very handy. Cloudflare Containers are controlled by Cloudflare Durable Objects, which are accessed by end users via Cloudflare Workers.

Exposing Ports

I came across the following in the documentation:
"Because all Container requests are passed through a Worker, end-users cannot make non-HTTP TCP or UDP requests to a Container instance."

A Minecraft server requires TCP 25565 (default port) to be accessible by players to connect and play on it. This means that a Minecraft server in a Cloudflare Container is not directly accessible by users.

Container Disk Storage

Another limitation is that container disk storage is ephemeral, meaning that it the disk will be a fresh disk every time the container starts up. A Minecraft server stores game world, player and configuration data, which can't be wiped every time the container starts, so this is a limitation.

Getting Around The Limitations

Exposing Game Server Ports

My approach was to use an SSH tunnel that the container connects to for forwarding the game server's port via the tunnel. I would then have a machine at the end of the tunnel to accept the container's connection. This machine can expose ports to the internet, so through this tunnel, the game server can be exposed to the internet. This does add some latency as the game server is essentially being relayed through the tunnel.

Data Persistence

Cloudflare offers a service called Cloudflare R2, which allows for object storage using AWS S3 APIs. I decided on using Cloudflare R2 to store the Minecraft server's data. The idea is that upon starting a container, the my API will check if there is any existing backup of the game server data, and if there is, it will pull that data from R2 into the container, and start the game server. When the container is instructed to stop, it would then compress the game server data and upload it into R2 for restoring on the next start.

Physical Location of the Container

I could not find any way to force the container to start at a specific location, so it chooses a random location in the world and starts it there (hello latency).

Architecture

Above is the main flow of communication. The Cloudflare Durable object acts as an intermediary between the Worker and Container.

Cloudflare Worker

I built the user-facing REST API with TypeScript, using the Hono framework, a modern, lightweight and fast framework and deployed it on Cloudflare Workers. I have a basic PostgreSQL 17 database and I use Prisma ORM to interact with it.

Cloudflare Durable Object

The Cloudflare Durable Object acts as a control plane to send HTTP requests to the container, start/stop it etc. I interact with this Durable Object through the Cloudflare Worker.

Cloudflare Container

This container uses the itzg/minecraft-server image with Docker. I have also built a simple Go REST API that goes with the container. I chose Go for the container's REST API because Go is simple to run in the container, as it doesn't have any runtime dependencies unlike other options like Python. This REST API is what the Worker makes requests to via the Durable Object using the containerFetch method in the Durable Object, which sends a request to the container.

Features

Generate an SSH key pair

The Go API inside the container generates an SSH ed25519 key pair, which is then securely stored in a secrets store through the Cloudflare Worker, allowing the container to reuse it when it starts again.

Tunnel (SSH, not Minecraft tunnels)

The REST API also provides a handler to start an SSH tunnel into the relay server. It uses the autossh command to establish the tunnel using the generated key pair. The relay server authorises the public key and the game server container uses the private key.

The relay server is what end-users of the Minecraft server connect to using their game client. They don't need to do anything extra.

For my testing, I used a Fedora virtual machine with VMWare, using the Bridged virtual network adapter to replicate a physical connection to my router. I then set up port forwarding for the VM's IP address to allow connections.

Backup/Restore

The disk in the containers is ephemeral. I don't want to start my Minecraft world all over again every time I start the container, so I built a handler in the container to compress the game server files into a ZIP file and upload them to Cloudflare R2 when the game server is stopped.

When the game server starts up, it will get the latest saved backup from R2, decompress it, write it to the Minecraft server's directory, and then start the game server, ensuring that it essentially picks up where it left off.

The Go application uses the AWS SDK to interact with R2.

WebSockets for Logs

The container can be connected to for viewing logs and sending commands through a WebSocket connection, which is initially started from the Worker, and funnelled into the container's WebSocket handler. The user can also send messages through the WebSocket connection to send commands into the game server.

The Go application uses the Gorilla WebSocket library:
https://github.com/gorilla/websocket

Minecraft Server

The Minecraft server runs in a screen session within the container.

The Control Plane: Hono + Cloudflare Workers

As described before, the Hono framework was used to build the user-facing REST API and it is deployed on Cloudflare Workers. The container is managed through this REST API.

I have added simple auth so I don't get random users messing with my containers.

Security Considerations

User-created commands

A user could send crafted input that attempts to escape the screen session and execute arbitrary shell commands. It may also be possible to escape the session with custom Minecraft server plugins.

To mitigate this, run the screen session under a restricted user with no system-level permissions.

SSH key pairs

Public/private key pairs should be stored securely. Store keys in a secure secrets manager (e.g. HashiCorp Vault). Limit file permissions and never expose keys in logs or API responses. Rotate keys periodically and restrict their scope to specific hosts or actions.

Extra Stuff

Adding authentication to the WebSocket connection

As you may or may not know, WebSocket connections, at least when made through browser APIs, does not allow setting an Authorization header, or really anything other than the socket address.

One approach is to pass the user's access token within the address, but this is not secure as many proxies log the URLs users access, which would expose the access token.

Another approach is to use a single-use token, or a very short-lived token that is also passed through the URL. This is more secure than the previously mentioned approach, but it can still give enough time for an attacker to steal the token. I am also using Cloudflare Workers, which is billed on CPU time, along with many other serverless services, and creating a new token, at least using JWTs, can increase CPU time.

I read about a way to pass data through headers when making a WebSocket connection a while ago. It involves passing the user's access token (or equivalent) through the Sec-WebSocket-Protocol header, which can be set when instantiating a new WebSocket in a browser for example. For testing, I used Postman's WebSocket feature and added the Sec-WebSocket-Protocol header with my access token as the value, and I set up my Worker to check this header for the /ws/logs endpoint.

Some Screenshots

I didn't build a frontend for this, so my "frontend" is Scalar API docs haha.

I am using Chanfana, which has automatic OpenAPI generation and works with Hono. The Scalar docs are using the generated OpenAPI schema.

Chanfana:
https://github.com/cloudflare/chanfana

Endpoint for starting the server

Endpoint for starting the server

Container instance running

Container logs

Note that I am logging the autossh command. I only did that for debugging purposes.

Container connected to relay

In-game

Note that the latency is very high as the container decided to start in Taiwan, although I am in Australia.

WebSocket connection

Conclusion

This project was able to get a Minecraft server running on Cloudflare's infrastructure using Cloudflare Workers, Durable Objects and Containers. I was able to successfully connect to the Minecraft server, play on it, stop the server, start it back up and continue where I left off.

I built a REST API with Hono for users/server admins to manage their game server, and a REST API with Go, which runs within the Docker container which also runs the game server application. This Go application, paired with the Hono application, let admins manage their servers.

Backup and restore functionality was also added using Cloudflare R2 and the AWS SDK to overcome the limitation of the container disk being ephemeral.

It was a fun project, but at the moment, I don't see it as a viable way to host Minecraft servers or anything that requires low latency between the server and users, as there does not seem to be a way to select a specific region or location for the container.

Another reason is that this requires another machine, whether its an AWS EC2 instance or a Raspberry Pi, there must be an SSH server to relay the game data to and from the internet. This is an additional cost, however I don't image it would need to be extremely high end as it is just acting as a relay, at least for low usage.

If Cloudflare lets us choose a location, this experiment could get a lot more interesting. Regardless, it is great to see how much can be done with Cloudflare services.

If you have any questions, please feel free to leave a comment!

Top comments (0)