An on demand Minecraft Server
Sometimes I play minecraft. Sometimes I play a lot of minecraft and sometimes I just stop playing for months. Lately when I do play, I’ve bene playing with a slightly modified version of Tekkit and running my own server. I have a VPS that I probably under use, so I decided to run the server there for when I do play with my friends.
My VPS is not very powerful, and running a Minecraft server when I stop playing for months is a huge waste of resources. I sought a way to automatically bring the server up when I wanted to play and shut it down when I wasn’t playing for a while.
Intro and credits
Most of this I gleemed by reading this article at planetminecraft.com. It’s quite well written, but I made my own changes to suite my needs and expanded on some things. In particular, I wanted more abstraction, the website seems to mangle the bits of code posted there, and a couple of things were left unexplained.
I also heavily referenced this wiki to understand the minecraft ping protocol to get MOTD even when the server is down, letting users know the server is starting up.
All this code is available on github.
Disclaimer: This code probably has at least one bug.
Assumptions
A couple of notes before we get started. I run Arch Linux on all my machines, including my server. I use cronie
as my crontab implementation. These scripts make use of lots of ‘standard’ tools such as a screen
, sed
, grep
, and tr
, and ‘less standard’ ones like netcat
, pgrep
, and xinetd
. You don’t really need to understand them use this, but I won’t explain them here. I will assume you know how to run and install a Minecraft server. These scripts should work with any Minecraft server; I personally use Tekkit.
This article, and the scripts to a lesser extend, expect files to be in particular places. The only hard-coded path in the scripts should be /etc/tekkit-on-demand/config.sh
. The article expects the binaries to be installed in /usr/bin/tekkit-{start,idle}
, and the launch helper to be installed in /etc/tekkit-on-demand/launch.sh
The server: overview
The server runs in a screen
session. I previously used systemd
to manage the server, but there are a few advantages to running it in screen
. You can easily, and programatically, send commands to the server, and more easily filter logs, which I find necessary due to the absurd number of info messages. The server is run as an unprivileged user, but requires root
to launch it.
config.sh
The file config.sh
contains all the configuration variables, and the functions with the core commands for starting and stopping the server, and detecting when the server is idle.
The file is configured by simply setting the variables at the top of the file. The variables have sensible defaults seen later in the file. We will refer to some of the variables such as $SERVER_USER
in the rest of the guide.
Advanced configuration involved changing the functions start
, stop
, idle
, and debug
. The functions shouldn’t need to be changed unless your server is configured quite differently, or you want to avoid screen
, xinetd
, or some other vital piece explained in the rest of the guide.
#!/bin/sh
## Change these configuration variables. They should probably match server.properties
## Leave them blank if you think I'm a good guesser.
SERVER_ROOT=
SERVER_PROPERTIES=
LOCAL_PORT=
LOCAL_IP=
MINECRAFT_JAR=
MINECRAFT_LOG=
SESSION=
WAIT_TIME=
SERVER_USER=
LAUNCH=
START_LOCKFILE=
IDLE_LOCKFILE=
PLAYERS_FILE=
## NB: This default may not be sensible
JAVAOPTS=
JAVAOPTS=${JAVAOPTS:--Xmx2G -Xms1G -server -XX:+UseG1GC -XX:MaxGCPauseMillis=50 \
-XX:ParallelGCThreads=2 -XX:+DisableExplicitGC -XX:+AggressiveOpts -d64}
## TODO: Currenently not used. Need to recompute size and UTF-16BE
## encode the message, which is annoying
MESSAGE=
## Here be defaults
SERVER_ROOT=${SERVER_ROOT:-/srv/tekkit}
SERVER_PROPERTIES=${SERVER_PROPERTIES:-$SERVER_ROOT/server.properties}
LOCAL_PORT=${LOCAL_PORT:-$(sed -n 's/^server-port=\([0-9]*\)$/\1/p' ${SERVER_PROPERTIES})}
LOCAL_IP=${LOCAL_IP:-$(sed -n 's/^server-ip=\([0-9]*\)$/\1/p' ${SERVER_PROPERTIES})}
MINECRAFT_JAR=${MINECRAFT_JAR:-$SERVER_ROOT/Tekkit.jar}
MINECRAFT_LOG=${MINECRAFT_LOG:-$SERVER_ROOT/server.log}
SESSION=${SESSION:-Minecraft}
MESSAGE=${MESSAGE:-Just a moment please}
WAIT_TIME=${WAIT_TIME:-600}
SERVER_USER=${SERVER_USER:-tekkit}
LAUNCH=${LAUNCH:-/etc/tekkit-on-demand/launch.sh}
START_LOCKFILE=${START_LOCKFILE:-/tmp/startingtekkit}
IDLE_LOCKFILE=${IDLE_LOCKFILE:-/tmp/idleingtekkit}
PLAYERS_FILE=${PLAYERS_FILE:-/tmp/tekkitplayers}
...
Starting the server
Starting the server is tricky. We must ensure any user that tries to connect sees a message alerting them that the server is not up now, but will be shortly. We also need to be sure to start only one instance of the server. Finally, we have to route traffic between the server and the meta-server that is watching to start the server on-demand.
Start on-demand
To automatically start the server, we use xinetd
, a ‘super-server’ (or as I prefer, ‘meta-server’). The meta-server is a server that manages servers by binding to the server’s port, starting the server when a client attempts to connect, then forwarding all traffic to the server.
tekkit
:
service tekkit
{
type = UNLISTED
instances = 20
socket_type = stream
protocol = tcp
wait = no
user = root
group = root
server = /usr/bin/tekkit-start
port = 25565
disable = no
}
We install this file in /etc/xinetd.d/tekkit
, and have xinetd
reread configuration files, for instance, via systemctl reload xinetd
. This path is fixed by xinetd
. You must change port = ...
in this file if you change $SERVER_PORT
.
Now when someone tries to connect to your server on port 25565
, the meta-server will run the file /usr/bin/tekkit-start
. Note that since the meta-server is binding port 25565
, your server must use a different port. I use port 25555
, but you can configure this with $LOCAL_PORT
.
Screen and the server command
A typical Minecraft server is started with a command that looks something like /usr/bin/java $JAVAOPTS -jar $MINECRAFT_JAR nogui
. We add to this command a filter to filter out the INFO messages, and to capture the number of players. All output is first piped to a sed
script that watches for the response to a list
command. The list
command is a Minecraft server command that lists the number of players online. The number of players is captured to the file specified by $PLAYERS_FILE
. The remaining output is filtered through grep
to discard INFO messages. This is done in config.sh
in the start
function:
start() {
/usr/bin/java $JAVAOPTS -jar $MINECRAFT_JAR nogui 2>&1 \
| sed -n -e 's/^.*There are \([0-9]*\)\/[0-9] players.*$/\1/' -e 't M' -e 'b' -e ": M w $PLAYERS_FILE" -e 'd' \
| grep -v -e "INFO" -e "Can't keep up"
}
We want to run the server in screen
to allow issuing commands, such as list
, to the server. Unfortunately, screen
doesn’t appear to take a function as an argument. We use launch.sh
as a wrapper, and have screen
run launch.sh
as an unprivileged user called $SERVER_USER
.
launch.sh
: sh
#!/bin/sh
source /etc/tekkit-on-demand/config.sh
cd $SERVER_ROOT
start
The file /usr/bin/tekkit-start
is actually responsible for starting the server, and the screen
command appears in there. However, much more happens before starting the server…
Server starting message
When a player first connects, we do not want them scared away by a “Can’t reach server” message. We implement the minecraft server list ping response, details here, to give them a less scary message. This protocol is implemented in the function sign
in the tekkit-start
file.
tekkit-start
:
#!/bin/sh
source /etc/tekkit-on-demand/config.sh
sign(){
# Kick protocol start
echo -en "\xFF"
# Length in characters: (including protocol, MOTD, current, max players)
# 22
# |
echo -en "\x00\x22"
# UTF-16BE String: Protocol header
echo -en "\x00\xA7\x00\x31\x00\x00"
# Protocol version:
# 4 7
# | |
echo -en "\x00\x34\x00\x37\x00\x00"
# Minecraft version:
# 1 . 6 . 4
# | | |
echo -en "\x00\x31\x00\x2E\x00\x36\x00\x2E\x00\x34\x00\x00"
# MOTD: "Up in just a sec.."
echo -en "\x00\x55\x00\x70\x00\x20\x00\x69\x00\x6E\x00\x20\x00\x6A\x00\x75\x00\x73\x00\x74\x00\x20\x00\x61\x00\x20\x00\x73\x00\x65\x00\x63\x00\x2E\x00\x2E\x00\x00"
# Current Players:
# 0
# |
echo -en "\x00\x30\x00\x00"
# Max Players:
# 0
# |
echo -en "\x00\x30"
}
This implementation is kind of bad, with lengths computed and strings encoded by hand. Maybe I’ll fix it later. The comment above each string explain what the string means. The first two echo
s send binary strings representing the protocol start packet and the length of the message. The remaining echo
s send UTF16-BE encoded information, such as the minecraft version, the MOTD (the message displayed under the server name), and the number of players.
Control Flow
The rest of the tekkit-start
file is dedicated to control flow. We must ensure only one instance of the server is started, so we use pgrep
to ask if the $SERVER_USER
user has any process using the $MINECRAFT_JAR
. If the server is not running, we start the server, post ping response, and wait for the server to start responding. If the server is already up, we use nc
to route traffic between the server and meta-server.
To ensure every user continues to see the ping response while the server is starting but not yet responding, we add a $START_LOCKFILE
While the $START_LOCKFILE
exists, the only thing tekkit-start
will do is post the ping response.
...
if [ ! -f $START_LOCKFILE ]; then
touch $START_LOCKFILE
if ! pgrep -U $SERVER_USER -f "$MINECRAFT_JAR" >/dev/null; then
sudo -u $SERVER_USER -- screen -dmS $SESSION $LAUNCH
sign
while netcat -vz -w 1 localhost 25555 2>&1 | grep refused > /dev/null; do
debug "Connection refused"
sleep 1
done
debug "Deleting start lock"
/bin/rm $START_LOCKFILE
debug `[ -f $START_LOCKFILE ] && echo "Lockfile still exists"`
else
/bin/rm $START_LOCKFILE
debug `[ -f $START_LOCKFILE ] && echo "Lockfile still exists"`
exec sudo -u $SERVER_USER nc $LOCAL_IP $LOCAL_PORT
fi
else
sign
fi
Stopping the server
Stopping the server is easier than starting it. We want to stop the server when there have been no players online for some amount of time. However, we want to make sure not to stop too frequently, since a player may stop briefly to make food, do some work, or just get away from the computers for a little bit. Once we know the server is idle, we just stop it by issuing a stop
command to the server.
stop() {
screen -S $SESSION -p 0 -X stuff 'stop\15'
debug "Shit's going down"
}
This commands tells screen
to connect to the $SESSION
session, on window 0
, and stuff
the string stop\15
into the input buffer. The command stop
tells the server stop running. The final character \15
is the control character for enter/return, so this simulates typing stop
and pressing enter.
Detecting an idle server
We specify how frequently to perform an idle check using crontab
. If there is no one online during the check, the script will wait $WAIT_TIME
seconds and check again. If both checks pass then the server will shutdown.
We add the following to $SERVER_USER
’s crontab to run the idle check once an hour.
crontab -e
:
@hourly /usr/bin/tekkit-idle
To determine the number of users online via script, we have all logs filtered through the sed
script seen in start()
. The script looks for a particular server message, and dumps the number to the file $PLAYERS_FILE
.
We can force the server to output this message by using the list
command. Since the server is running in a screen
process, we can issue this command via screen -S Minecraft -p 0 -X stuff 'list\15'
. The command list
asks the server to dump the current number of players.
Before issuing the requre, we clear the file. After issuing the request, we wait until the file is not blank, so sed
must have found the message and dumped it to the file. This prevents race conditions. We read in and compare to 0. All this logic is implemented in the config.sh
function idle
. There is also a bunch of debugging information there, because I had trouble with sed
outputing invisible characters to the $PLAYERS_FILE
. We use tr -d [:cntrl:]
to remove these invisible control characters.
idle() {
echo -n "" > ${PLAYERS_FILE}
debug `cat ${PLAYERS_FILE}`
screen -S $SESSION -p 0 -X stuff 'list\15'
players=`tail -n 1 ${PLAYERS_FILE} | tr -d [:cntrl:]`
while [ -z ${players} ]; do
sleep 1
players=`tail -n 1 ${PLAYERS_FILE} | tr -d [:cntrl:]`
done
debug "There are ${players} players"
if [ "0" = "${players}" ]; then
debug "Idle"
true
else
debug "Not idle"
false
fi
}
Below is the idle detection script, called tekkit-idle
. The function idle
is implemented in config.sh
and returns true when no players are online. The reset of the script implements the logic I explained before: if the server is idle, i.e., no one is online, then wait $WAIT_TIME
seconds. If the server is still idle, shut it down.
tekkit-idle
:
#!/bin/sh
source /etc/tekkit-on-demand/config.sh
if [ ! -f $IDLE_LOCKFILE ]; then
touch $IDLE_LOCKFILE
debug "No lock file, checking!"
if idle; then
debug "Idle, waiting!..."
sleep $WAIT_TIME
if idle; then
debug "Still idle, stopping!"
stop
fi
fi
/bin/rm $IDLE_LOCKFILE
fi
debug "Idle check complete"