Minecraft On-Demand

I recently started a private Minecraft server on my Linode. I was dismayed to discover that it caused the CPU graph on the Dashboard to spike to 100% despite the CPU usage as reported by top being only around 40-50% when a couple of players were present and around 10% when idle. I spent a lot of time worrying about this and even opened a support ticket to make sure I wasn't doing anything terribly wrong. Support pointed me to a post that discussed the same issue. The conclusion appears to be that it's a bug either in Minecraft or Java and it is confusing the reporting but isn't a real problem.

Despite this, I decided I didn't really like seeing the CPU graph maxed out all of the time, nor did I like the idea that Minecraft sits there consuming 10% of the CPU even when idle. While looking for solutions I ran across a bug report on Mojang's bug tracker about this sort of thing. Naturally they marked it resolved (when it clearly is not), but a person who left a comment on that bug had a rather inspiring solution: Pause the Minecraft Java processes when no one is connected and wake it up when a connection is attempted.

I loved the idea but decided that maybe I could improve on the implementation - so here's my take:

We'll be making a script that manages the Minecraft server by periodically checking for established connections on port 25565 (the standard Minecraft port). When there are no connections, the script will send a kill -STOP signal to all of the Minecraft server's Java processes. This immediately suspends them without actually terminating them. To wake them back up again, we will use knockd to listen for connection attempts on port 25565. When such an attempt is made, knockd will simply touch a signal file that our script is watching. When the change is detected, the script sends kill -CONT to all of the processes and the server wakes up and accepts the new connection request.

Before you begin, you must have a working Minecraft server install - otherwise this is all pointless. I initially followed Linode's own guide so these instructions are going to be assuming that you've done it the same way - specifically that there is a minecraft user account and that the Minecraft files are all sitting in /home/minecraft. If you've done something different, you'll have to adjust accordingly as you go along.

First, let's install the tools we'll be using:

$ sudo apt-get install inotify-tools net-tools knockd

We are going to tell knockd to touch a signal file whenever a connection is attempted on the Minecraft port. Later we will write a script that watches for changes to that file which will be the signal to wake up the sleeping Minecraft processes. By default, knockd comes with a configuration file that includes some rules for opening and closing SSH ports when certain sequences of knocks are detected (this is, after all, what knockd is actually for). Unless you're already using this, we don't want to accidentally turn that on and lock ourselves out or something silly like that.

Edit the knockd.conf file to remove those rules and add our own:

$ sudo nano /etc/knockd.conf

Make extra sure that you remove the SSH configuration sections (assuming you don't use them), and then add our new configuration. The file should look something like this:

[options]
    UseSyslog

[WakeMinecraft]
    sequence = 25565
    seq_timeout = 1
    tcpflags = syn
    command = touch /run/wakeup-minecraft

This tells the service to run the command touch /run/wakeup-minecraft whenever something happens on port 25565. Specifically, we only want to trigger our action when the TCP SYN packet is received. The SYN packet is the first packet in a TCP sequence and it only occurs once at the start of a connection attempt.

Now that knockd has been configured for our purposes, we must edit another file to allow it to actually start:

$ sudo nano /etc/defaults/knockd

Change START_KNOCKD=0 to START_KNOCKD=1 and save the file.

Now we can fire up the service:

$ sudo systemctl start knockd.service

And if you want the service to start when the system reboots (which seems likely), run:

$ sudo systemctl enable knockd.service

At this point you should be able to fire up Minecraft (the game) and attempt to connect to your server (even if it's not running). If knockd is configured and working properly, you should see the file /run/wakeup-minecraft appear the moment the game attempts to connect or ping your server.

Now that we have a file we can watch for changes, we need a script to manage the lifecycle of the Minecraft server. This script will be owned and operated by your minecraft user, so switch to the minecraft user and make sure you're in the home directory (or wherever your server files are) and make a new file:

$ nano run-and-monitor-minecraft.sh

#!/bin/bash

####################################
# config
####################################

MINECRAFT="java -Xmx1024M -Xms1024M -jar minecraft_server.1.15.1.jar nogui"
SIGNAL_FILE=/run/wakeup-minecraft
IDLE_TIMEOUT=180
SERVER_PORT=25565

####################################
# script
####################################

# launch Minecraft in the background
eval "$MINECRAFT" &

# save Minecraft's PID so we can stop/resume it
PID=$!

pause_minecraft_if_unused() {
  if ! [[ `netstat -tn | grep $SERVER_PORT | grep ESTABLISHED` ]] &>/dev/null; then
    if [ `ps -o state= -p $PID` != T ] &>/dev/null; then
      echo "Pausing Minecraft"
      kill -STOP $PID $(pgrep -P $PID)
    fi
  fi
}

resume_minecraft_if_paused() {
  if [ `ps -o state= -p $PID` = T ] &>/dev/null; then
    echo "Resuming Minecraft"
    kill -CONT $PID $(pgrep -P $PID)
  fi
}

cleanup() {
  resume_minecraft_if_paused
  echo "Quitting Minecraft"
  wait
}

# prepare for errors
trap "exit" INT TERM ERR
trap "cleanup" EXIT

monitor() {
  # monitor for as long as the Minecraft process exists
  while [ `ps -o state= -p $PID` ] &>/dev/null; do
    # ensure the signal file exists before we attempt to watch it for changes
    if [ -f "$SIGNAL_FILE" ]; then
      # watch the signal file for changes
      inotifywait -qq -t $IDLE_TIMEOUT -e attrib "$SIGNAL_FILE"

      # if the timeout occurs, it means the signal file has not been touched for awhile
      # in that case, we check if there are any connections on the Minecraft port, and if not we put Minecraft to sleep
      # otherwise we wake up the Minecraft process since the signal file has been modified
      case $? in
        0) resume_minecraft_if_paused ;;
        2) pause_minecraft_if_unused ;;
        *) kill 0 ;;
      esac
    else
      # if there is no signal file, we'll keep trying to put Minecraft to sleep as we wait for one to appear
      while [ ! -f "$SIGNAL_FILE" ]; do
        pause_minecraft_if_unused
        inotifywait -qq -t $IDLE_TIMEOUT -e create -e moved_to "$(dirname $SIGNAL_FILE)"
      done

      # a signal file appeared, so wake up Minecraft and proceed with the normal monitoring loop
      resume_minecraft_if_paused
    fi
  done
}

# wait a bit to give Minecraft a chance to start up before we think about pausing it
sleep $IDLE_TIMEOUT

# start monitoring
monitor

I'm no Bash scripting wizard (indeed I had to learn a LOT just to come up with this), but what the script does is launch Minecraft using the command specified in the variable MINECRAFT. It then keeps track of the process ID so that it can send signals to it and any child processes that are spawned by it (such as other threads). As the Minecraft server starts launching in the background, it begins to monitor the signal file /run/wakeup-minecraft for an attribute change or, if the file doesn't exist, wait for it to show up. (This is the same file knockd is set to touch whenever activity occurs on port 25565.) After waiting up to 180 seconds, either inotifywait has exited with a status code or the file has appeared. If the signal file was not touched during the time period or did not exist, then the script checks for established network connections on port 25565 using netstat. If there aren't any connections, the Minecraft processes are all sent the STOP signal and the loop repeats. If the signal file attributes have changed (which is what touch does) or the signal file has just appeared, the script will instead wake up the Java processes which will then be able to accept the incoming connection request.

Note that you will likely need to modify the MINECRAFT line to use the Jar for the version of Minecraft you have installed and possibly to adjust the memory requirements. If you're running on a Nanode, you're probably going to want to cut these memory values almost in half. Since you should already have configured a Minecraft server, just use whatever you had in there before. :)

Obviously before we can run the script we have to make it executable:

$ chmod +x run-and-monitor-minecraft.sh

At this point you should be able to run the script as the minecraft user. It should start up and, after 180 seconds without a connection, it should put itself to sleep. If you connect with the game, it should immediately wake up the processes.

One side effect of this script is that it does not pass STDIN to the Minecraft server console anymore, so while it will print the Minecraft output, you can't actually run any commands from here. I'm sure there's some way to do this - but I couldn't figure it out. I just made myself an op in the server's server.properties file and administer from within the game itself. There are probably other ways to do this.

Speaking of the server settings, there is one important one that we must edit before this whole scheme works in the long run. Minecraft has a builtin watchdog that will kill the server if too much time has passed while attempting to process the game ticks. Since we'll be pausing the process for potentially a very long time (hours, days, etc) when it's not in use, this watchdog will end up killing the server the moment it wakes back up again because too much time will have passed. To fix this, we'll just shut it off.

Kill the script with Ctrl-C if you had left it running, and then edit the Minecraft server's server.properties file:

$ nano server.properties

Look for the max-tick-time setting and set it to max-tick-time=-1 and then save the file.

You could stop at this point and you could just run the run-and-monitor-minecraft.sh script in a screen session like the original Minecraft setup guide suggests, but I thought it might be fun to go one step farther and make this whole setup a proper service on our server that we can start and stop like any other.

To do this, I'm going to use Supervisor because I happen to be minimally familiar with it. I'm sure there are countless other ways.

First let's install it:

$ sudo apt-get install supervisor

Now we need to add a configuration file so it knows the name of the service we're adding and what to do when we want to start it:

$ sudo nano /etc/supervisor/conf.d/minecraft.conf

[program:minecraft]
command=/home/minecraft/run-and-monitor-minecraft.sh
directory=/home/minecraft
user=minecraft
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true

Obviously if you are using a different user account or location for your server files and scripts, you'll have to adjust these accordingly.

Next, make sure you aren't already running your server. If you are, kill it first and tell the supervisor service to reload it's configuration:

$ sudo supervisorctl reload

And just like that, Minecraft will now launch using our new run-and-monitor-minecraft.sh script. After a few minutes, it should put itself to sleep and wake only when needed!

You can now easily stop or start it at any time using these commands:

$ sudo supervisorctl stop minecraft
$ sudo supervisorctl start minecraft

Supervisor places log files in /var/log/supervisor/, so you should be able see them there if you want to keep an eye on what the kids are saying in chat or whatever.

And that's it - we now have Minecraft running as a service, it should relaunch when the system reboots, and it should pause and stop sucking up electricity and CPU when not in use. Tada!

1 Reply

Thank you for this write-up.
Your solution works a treat! My Linode CPU usage has gone back to it's normal idle state. The fact that Mojang have marked this as resolved in their bug tracker is laughable.
I skipped the automated service part, but this is still valuable information to have. :)

Reply

Please enter an answer
Tips:

You can mention users to notify them: @username

You can use Markdown to format your question. For more examples see the Markdown Cheatsheet.

> I’m a blockquote.

I’m a blockquote.

[I'm a link] (https://www.google.com)

I'm a link

**I am bold** I am bold

*I am italicized* I am italicized

Community Code of Conduct