I recently set up a Minecraft server again, I usually do this by simply running the downloaded .jar file in a screen session. This time though, I did it a little bit better. In this post I’ll describe what I consider to be the perfect setup.

Systemd

I started by creating a systemd configuration for the server. Since I was using screen, I also found a way to extract the screen PID from inside the launch script and write it to a file so systemd can read it. The snippet for this is as follows:

screen -ls | grep .minecraft | grep -oP "([0-9]*(?=.minecraft))" > minecraft.pid

This will fetch all screens called minecraft (which should be only one) and writes its PID to the file minecraft.pid in the current directory.

The configuration file for systemd is pretty small and IMHO self explaining. The only things that need to be changed are the path where the server is installed and the user it shall run as (never run your services as root if possible).

[Unit]
Description=Minecraft Server
After=syslog.target network.target
Conflicts=minecraft-server.service

[Service]
Type=forking
User=mc-server
Group=mc-server
PIDFile=/home/mc-server/minecraft/minecraft.pid
ExecStart=/home/mc-server/minecraft/start-server.sh
ExecReload=/home/mc-server/minecraft/backup-server.sh
ExecStop=/home/mc-server/minecraft/stop-server.sh

[Install]
WantedBy=default.target

Start script

To start the server, I wrote a small bash script that resides inside the main minecraft folder. It basically kills leftover screen sessions, starts a new one and then starts the server inside it. After that it fetches the PID of the screen using the snippet above and prints out the PID file’s content (for debugging reasons). You also need to update the paths here.

#!/bin/bash
cd /home/mc-server/minecraft
echo "cleaning leftover screens..."
screen -wipe
echo "starting minecraft screen..."
screen -dmS minecraft bash
screen -S minecraft -X stuff 'java -Xmx8G -Xms2G -jar server.jar nogui'$'\n'
echo "fetching pid..."
screen -ls | grep .minecraft | grep -oP "([0-9]*(?=.minecraft))" > minecraft.pid
cat minecraft.pid

Stop script

The stop script will notify anyone currently playing on the server using the say command and stops the server afterwards. Please note that it might be unsafe just to kill the screen, therefore the script sends the actual stop command to the server to shut it down. You also need to update the paths here.

#!/bin/bash
set -euo pipefail
cd /home/mc-server/minecraft
screen -S minecraft -rX stuff 'say Server stopped by script! Stopping in 5...'$'\n'
sleep 5
screen -S minecraft -rX stuff 'stop'$'\n'
exit 0

Backups

Since I am a git junkie, I also backup the minecraft server using git (that way I could easily roll back changes made by griefers or similar).

I decided to link this script to the reload command so I can call systemctl reload minecraft.service from a cronjob to do automatic backups.

The backup script will stop the server, commit all changes made, sync with the server using git pull --rebase (this allows for “updates” to the minecraft server itself by using the GitLab WebIDE), push the new commit and start the server again. Note that you also need to update the paths and the commit author here.

#!/bin/bash
set -euo pipefail
cd /home/mc-server/minecraft
screen -S minecraft -X stuff 'say Backup starting...'$'\n'
screen -S minecraft -rX stuff 'stop'$'\n'
sleep 2
git add --all
git commit -m "scripted backup" --author="Minecraft Backup <minecraft@example.com>"
git pull --rebase
git push -u origin master
echo "restarting service..."
screen -S minecraft -X stuff 'java -Xmx8G -Xms2G -jar server.jar nogui'$'\n'

Maps

What is a cool minecraft server without a Google Maps-like website that shows the available worlds?

You may already have heard of overviewer. It’s an application that renders the map described above. Unfortunately the setup described in their documentation was a bit too annoying for me and also didn’t fit well into my automagic backup procedure (I’d have to run the renderer myself or start it using a cronjob) so I decided to build a Docker container for it and run it using GitLab CI.

The biggest problem with this (and the reason why I’m unable to just opensource this) is that overviewer needs an original minecraft.jar whose distribution is probably not allowed according to Microsoft’s EULA (please don’t try to tell me that “Minecraft is still made by Mojang”, it’s all M$ now.)

On macOS you can find the file (for version 1.13.1) here: ~/Library/Application\ Support/minecraft/versions/1.13.1/1.13.1.jar

On Linux it should be somewhere inside ~/.minecraft.

On Windows, searching %APPDATA% might be the way to go but I don’t know (and don’t really want to).

The last thing you need is the Dockerfile (called Dockerfile) which should contain the following:

FROM ubuntu:latest

RUN apt-get -qqq update && \
apt-get -yq upgrade && \
apt-get -yq install build-essential git python-willow python-dev python-numpy && \
apt clean

RUN git clone git://github.com/overviewer/Minecraft-Overviewer.git /overviewer && \
cd overviewer && \
git checkout minecraft113 && \
python2 setup.py build && \
python2 setup.py install && \
mkdir -p /root/.minecraft/versions/1.13.1/

ADD 1.13.1.jar /root/.minecraft/versions/1.13.1/

CMD bin/bash

If you put those two files inside a new folder and run docker build -t lerk/overviewer, you’ll have the docker image available. You can then run it using: docker run -it lerk/overviewer

Additionally, you will need a configuration file for overviewer, which is called overviewer.conf. An example configuration might look like this:

worlds["My World"] = "world"
worlds["My Nether"] = "world_nether"

renders["day"] = {
    "world": "My World",
    "title": "Daytime Render of My World",
    "rendermode": "smooth_lighting",
    "dimension": "overworld",
}

renders["night"] = {
    "world": "My World",
    "title": "Nighttime Render of My World",
    "rendermode": "smooth_night",
    "dimension": "overworld",
}

renders["nether"] = {
    "world": "My Nether",
    "title": "Render of My Nether",
    "rendermode": "nether_smooth_lighting",
    "dimension": "nether",
}

outputdir = "mcmap"

Please note that this config file will probably not work with your setup. You should consult the official documentation on this topic to get it working the way you want it to.

You can then start the docker container, pull the minecraft server repo (if you’ve put it in git) and render your first map by running the following command from inside the repo folder:

overviewer.py --config=overviewer.conf

If you used either the configuration above (and it works) or used the outputdir parameter in your own config, you need to mkdir the directory before starting the render or it will fail.

GitLab

Now to wrap this whole thing up, I want the renderings to start automatically after every backup (push into git). To do this I use my GitLab instance (to host the repo itself), GitLab CI (to run the renderer) and GitLab Pages (to host the resulting map).

All of this complicated sounding stuff is as easy as writing yaml (which means it’s not. At all!) and you simply need to create a .gitlab-ci.yml inside the root folder of your repo:

image: lerk/overviewer

stages:
  - render
  - deploy

map:
  stage: render
    script:
      - mkdir mcmap
      - overviewer.py --config=overviewer.conf
  artifacts:
    paths:
      - mcmap/

pages:
  stage: deploy
  dependencies:
    - map
  before_script: []
  script:
    - mv mcmap/ public/
  artifacts:
    paths:
      - public

expire_in: 30 days
only:
  - master

The image parameter tells GitLab CI to run the build inside the overviewer container we’ve just built. Obviously you need GitLab CI Runners that are capable of running Docker.

The map job does the rendering itself. It simply runs overviewer and sets the artifacts property to tell GitLab to keep the render result.

The pages job uses the artifacts from the map job (by using the dependencies property), copies it into a path (afaik the only one) GitLab Pages uses to search for deployable stuff.

After a pipeline completed successfully you should be able to see the updated map on the Pages-URL of your repo.