Scale-to-Zero Minecraft server with Terraform and Fly Machines

Minecraft Steve in a hot air balloon that has the face of a creeper
Image by Annie Ruygt

I’m Dov Alperin. I wrote, and currently maintain, the official Fly.io Terraform provider. You don’t need Terraform to run scale-to-zero Minecraft on Fly Machines, but it makes configuration and resource provisioning a breeze. I admit I’m totally biased. (It’s true, though.) Start at the beginning with Fly.io, or jump right in here.

Running a Minecraft server for friends has become an archetypal first foray into the workings of the Internet. For some it’s learning to expose the tender underbelly of a home network to outside connections. For others it’s exploring the world of VMs, SSH, and infinite VPS options.

For me, as for so many, a Minecraft server was an early experience of running a “production” web service—one that others consumed and “depended” on. Mine was a DigitalOcean droplet held together with glue and duct tape.

Just a few years of experience (and a gig at a cloud-compute company) later, here’s my new take on this: an over-engineered, scale-to-zero Minecraft server running on a Fly Machine.

Scale-to-Zero?

Imagine this: you’re middle-school me and your Minecraft server has picked up a few more Daily Active Users than you’d expected. RAM is running low and the mortification of disappointing your peers is fast approaching as VPS resource utilization creeps up.

You scale the VPS up: more vCPUs, more RAM, smoother gameplay. You are now munching hungrily through the free tier of your hosting provider, or worse, paying money to keep your friends in enchanted boots and rotten flesh. Wouldn’t it be awesome if the munching could stop when nobody’s actually playing? Even better if the VM could start up again and carry on automatically when someone attempts to connect again.

This is the fundamental idea of scale-to-zero on Fly Machines: shut them down when no one is using them, but start them back up again when the user needs it, fast enough that no one is ever the wiser.

The plan

Our magic scale-to-zero Minecraft server takes a few ingredients:

  • Terraform, to abstract the Fly.io API into a declarative configuration file. We’ll configure the app and specify the resources to provision within main.tf. Then terraform apply will make it all happen. You can deploy the same app with flyctl. But Terraform is what my whole gig at Fly.io is about! Naturally I’m going to take advantage of it here.
  • Fly Machines: Firecracker VMs you can start and stop fast with a REST API.
  • Geoff Bourne’s (itzg) minecraft-server Docker image, for a well-tested and flexible way to run a Java Minecraft server, featuring a configurable Autostop feature that will automatically shut down the server if people haven’t used it in a given amount of time. This is what allows for our server to scale to zero. Thanks to itzg for working with me to get it working smoothly on Fly.io!

Getting started

If you don’t have Terraform yet, now’s a good time to install it.

Set the FLY_API_TOKEN environment variable. The Terraform provider uses this to authenticate us to the Fly.io API:

export FLY_API_TOKEN=$(flyctl auth token)

In a new terminal, open a proxy to give Terraform access to the internal APIs we’ll be using. Leave it open:

flyctl machine api-proxy

Create a new directory to work in:

mkdir tf-fly-minecraft && cd tf-fly-minecraft

Then let’s start our Terraform prep by creating a file called main.tf where we can import the Fly.io provider:

terraform {
  required_providers {
    fly = {
      source = "fly-apps/fly"
      version = "0.0.16"
    }
  }
}

provider "fly" {}

With this in place, run terraform init to set up your workspace.

It’s just a few steps to get started with Terraform and Fly Machines

Learn more

Let’s Build!

We are going to create four different resources:

  1. A Fly.io app, which is a sort of administrative umbrella that the VM will belong to;
  2. a Fly Machine that runs our server inside the app;
  3. a Fly Volume that our Minecraft world persists to; and
  4. a public IP address so that the player’s Minecraft client actually has something to connect to!

We will use the following assumptions for now:

  1. The app name is flymcapp (replace this with a name of your own), and
  2. we’re deploying in region yyz (replace this with somewhere near you).

Add the following blocks to the main.tf we created earlier:

resource "fly_app" "minecraft" {
  name = "flymcapp"
  org  = "personal"
}

resource "fly_volume" "mcVolume" {
  app    = "flymcapp"
  name   = "mcVolume"
  size   = 15
  region = "yyz"

  depends_on = [fly_app.minecraft]
}

resource "fly_ip" "mcIP" {
  app  = "flymcapp"
  type = "v4"

  depends_on = [fly_app.minecraft]
}

resource "fly_machine" "mcServer" {
  name   = "mc-server"
  region = "yyz"
  app    = "flymcapp"
  image  = "itzg/minecraft-server:latest"

  env = {
    EULA                    = "TRUE"
    ENABLE_AUTOSTOP         = "TRUE"
    AUTOSTOP_TIMEOUT_EST    = 120
    AUTOSTOP_TIMEOUT_INIT   = 120
    MEMORY                  = "7G"
    AUTOSTOP_PKILL_USE_SUDO = "TRUE"
  }

  services = [
    {
      ports = [
        {
          port = 25565
        }
      ]
      protocol      = "tcp"
      internal_port = 25565
    }
  ]

  mounts = [
    { path   = "/data"
      volume = fly_volume.mcVolume.id
    }
  ]

  cpus     = 4
  memorymb = 8192

  depends_on = [fly_volume.mcVolume, fly_app.minecraft]
}

The first block creates the Fly.io app, as you might guess. From there we have blocks that create a 15GB persistent storage volume and an IPv4 address.

Now we get to the meat of it: the fly_machine block. We start off by defining some basics: the machine name, what app it belongs to, what region it should run in, and what image it should run. In this case we use the super awesome minecraft-server Docker image from itzg.

The env block sets environment variables used by minecraft-server for configuration; for example, we’re setting the Autostop feature to shut down the VM when no one’s been connected for 120 seconds.

The services block exposes port 25565 to the outside world via the IP we defined earlier, and the mounts block connects the previously defined volume to our machine.

You may have noticed the MEMORY environment variable that we set to "7G". A Minecraft server wants a fair amount of memory, and some CPU oomph to match. So we specify vCPUs and 8G of RAM for this VM.

Finally, with depends_on we tell Terraform to make sure the app and the volume are in place before trying to start a VM.

Some Warnings

Access

As we configured it here, anyone can join the server. That’s probably not what you want! Check out the documentation to find out how to set up an allowlist using environment variables.

Costs

Minecraft servers aren’t exactly lightweight. Or rather: Java isn’t exactly lightweight. The example code creates a machine with 4 shared vCPUs and 8GB of RAM, and a 15GB storage volume. Vanilla Minecraft should still work fine, if a bit slower, if you tweak down the resources a bit. But we’re well outside of “free allowances” territory.

This is why the “scale to zero” aspect of this project is so useful, of course! However, any TCP traffic will wake up the machine, including things like port scans. It’ll go back to sleep, but the surefire way to prevent it incurring any further costs is to destroy the app. If you’re not going to use the server again, you’ll want to do this anyway, so you don’t have to pay for the storage volume.

Let’s Play!

Once you have finished tweaking anything you want to tweak in the Terraform file, go ahead and run terraform apply (and confirm when it prompts you) to create all the resources.

Once the command stops running, open up your Minecraft Java Edition installation, head to the multiplayer screen and connect to flymcapp.fly.dev (once again replacing it with the app name you chose earlier) and find a tree to cut down with your fist!

While it defaults to the latest “Vanilla” server, docker-minecraft-server can be configured to run any number of modded servers. Check out the README for configuration options.

Once you have played around for a few minutes, try quitting out of Minecraft and watch the logs on the Monitoring tab of the Fly.io dashboard. You’ll see that once the configured timeout is hit, it will shut itself down. Try connecting again, you’ll see the machine automatically start itself back up. Cool huh? :)

If you are done with this guide and don’t intend to use the server again, go ahead and destroy the app. We have a cornucopia of tools for destruction! Since you created this app with Terraform, you can use terraform destroy; for any Fly.io app, including this one, there’s also fly apps destroy; or you can hit the red “Delete app” button under your app’s Settings tab in the Fly.io dashboard. Check in your dashboard, or use fly apps list, to check that it’s gone.

A rudimentary version of the Fly.io logo built in Minecraft