Hacktoberfest: Minecraft Server

Hacktoberfest is upon us! This month I’m hacking on small projects each week and sharing them.

Previously…

A few weeks ago we found a problem with our GitLab Runner and fixed it. This week, we attempted to make a persistent Minecraft Server using a Dockerfile and the new GitLab Runner to deploy it. We hope to get backups running on the Minecraft Server.

Our Minecraft Server we managed to get working was working great… until we realized it didn’t have any backups. We tried looking for something akin to the WordPress Backup container solution. This didn’t quite pan out as it required a bit of container-to-container communications. I’d like to scale Minecraft hosting out, so while this is a solution, it isn’t a very clean one. Plus, I don’t really want to rely on a 3rd party to update the Dockerfile. So, here we are.

Redefined Requirements

Knowing what we want is half the battle. Figuring out how to do it is the actual hard part. So, we kicked back, grabbed some cookies, and started to think. What do we really want in a perfect Minecraft Server?

  1. We want maximum uptime. If there’s an update, rebooting should pick it up. Done!
  2. We want security. If we need to ban someone or whitelist someone this should persist across reboots. TODO
  3. We want safety. Rebooting should reload the existing world. If something corrupts it, we should be able to recover from a previous backup. TODO

Safety First

For this week, we focused on safety. We want to save our hard work building amazing things so we don’t lose it unexpectedly. To do this, we will need to safely stop the auto save, manually save the world state, back up all of the world files, then start the auto save. This is ideally scheduled as some sort of scheduled task that kicks off every day (or hour). To have the server interact with Minecraft, we will need some sort of RCON utility. So. we leveraged out new-fangled GitLab Runner to help us out.

Getting an RCON utility into a Docker image seemed rather straight-forward. Go get it, make it available to the build context, then copy it to the image giving it executable permissions. Seems easy enough, we can eve use GitLab artifacting since its in the same pipeline!

rcon-setup:
  stage: stage
  image: golang:latest
  script:
    - "go get github.com/SeerUK/minecraft-rcon/..."
    - "go install github.com/SeerUK/minecraft-rcon/..."
    - "mkdir bin"
    - "cp $GOPATH/bin/minecraft-rcon ./bin"
  artifacts:
    paths:
      - bin/

Here we have a stage (conveniently called stage. I know, so creative!) this runs on the latest Go container and simply pulls the source code to the local Go source path then compiles and installs the binary to the Go binary path. We copy it to the bin directory and artifact it! Now the artifact is in GitLab and is available to downstream dependencies. Let’s build the Docker image!

build:
  stage: build
  image: docker:latest
  dependencies: 
    - stage
  services:
    - docker:dind
  before_script:
    - "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY"
  script:
    - "docker build -t ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME} --pull ."
    - "docker push ${CI_REGISTRY_IMAGE}:${CI_COMMIT_REF_NAME}"
  after_script:
    - "docker logout ${CI_REGISTRY}"
  tags:
    - docker

Simple stuff here. Let’s take a look at the Dockerfile itself

FROM alpine:latest
ARG MC_VERSION=1.13.1
ARG MC_JAR_SHA1=fe123682e9cb30031eae351764f653500b7396c9
ARG JAR_URL=https://launcher.mojang.com/mc/game/${MC_VERSION}/server/${MC_JAR_SHA1}/server.jar
ARG MIN_MEMORY='256M'
ARG MAX_MEMORY='1024M'
ARG MC_CLIENT="c2technology"
ENV CLIENT ${MC_CLIENT}
ENV _JAVA_OPTIONS '-Xms${MIN_MEMORY} -Xmx${MAX_MEMORY}'
RUN mkdir -pv /opt/minecraft /etc/minecraft
RUN adduser -DHs /sbin/nologin minecraft
COPY bin/minecraft-rcon /usr/bin/minecraft-rcon
COPY backup /usr/bin
COPY entrypoint.sh /etc/minecraft
RUN apk add --update ca-certificates openjdk8-jre-base tzdata wget \
    && wget -O /opt/minecraft/minecraft_server.jar ${JAR_URL} \
    && apk del --purge wget \
    && rm -rf /var/cache/apk/* \
    && chown -R minecraft:minecraft /etc/minecraft /opt/minecraft \
    && chmod +x entrypoint.sh
EXPOSE 25565
USER minecraft
WORKDIR /etc/minecraft
ENTRYPOINT ["./entrypoint.sh"]

Starting with a minimal Linux Alpine container, we set some arguments to the Dockerfile. These can be overwritten as arguments passed to the docker build command. They must be defined in the Dockerfile in order to override them. We have some reasonably safe defaults here. We set some environment variables in the resulting container, make a directory, add a user, then we copy the RCON Go binary (from the artifacts copied into the Docker build context by GitLab’s artifact system) over to the container as well as the backup script we wrote. Then we install some dependencies, expose the Minecraft server port, switch to the Minecraft user, set the working directory, then run the entrypoint.sh script. Let’s take a look at that entrypoint.

echo 'eula=true' > /etc/minecraft/eula.txt
crontab -l | { cat; echo "0 */6 * * * backup"; } | crontab -
java -jar /opt/minecraft/minecraft_server.jar nogui

Not too complicated. This auto-accepts the EULA (Minecraft requires this to run) then sets up a job that runs every 6 hours to execute a backup command. Then finally runs the Minecraft server. This is what we wanted to be able to do in the first place — back things up on a schedule. We could have the scheduled interval for running the backup command configurable, which we will most likely do after we get this thing working (this is Hacktoberfest after all). So… let’s take a look at that backup script.

#!/bin/sh
minecraft-rcon save-off
minecraft-rcon save-all
tar czf /opt/backups/$(date +%Y-%m-%d)-mc-${CLIENT}.tar.gz /opt/minecraft/
minecraft-rcon save-on

Easy peasy! using that new minecraft-rcon binary, we turn automatic saving of the Minecraft world off so we can access it without it changing on us (and corrupting backup). We make one final save, tar it all up, then turn automatic saving back on. This seems to be the right thing to do so we don’t corrupt the world or save a corrupted version. We’ll see if this actually works when we get it running. If not, this is the file we can update to get it to correctly work — even if it means stopping the Minecraft service then restarting it.

Now that we have the Docker container published to our repository, we can update the existing Minecraft Server YAML to use it!

deploy:
  script:
    - docker pull minecraft-docker:latest
    - docker exec minecraft backup
    - docker stop minecraft || true
    - docker rm minecraft || true
    - docker run -d --name minecraft -p 25565:25565 \
        -v minecraft-world:/opt/minecraft/data/world \
        -v minecraft-config:/opt/minecraft/config \
        -v minecraft-mods:/opt/minecraft/mods \
        -v minecraft-plugins:/opt/minecraft/plugins \
        --restart always minecraft-docker:latest
    - docker cp ./config/* minecraft:/opt/minecraft/config/
    - docker cp ./data/* minecraft:/opt/minecraft/data/
    - docker cp ./mods/* minecraft:/opt/minecraft/mods/
    - docker cp ./plugins/* minecraft:/opt/minecraft/plugins/
    - docker exec minecraft backup
    - docker restart minecraft

We kick things off by pulling the latest minecraft-docker image. This will pull the private repository image we just published into the local Docker-in-Docker container that’s running this build. Then we backup the existing world if it exists before stopping the current Minecraft server. After that, we remove it and create a new container with various mounts. We then copy over the configurations and anything else we have version controlled before backing it up once again and restarting it. We back it up so many times right now because we’re not sure if this will corrupt the world data. Once we do know what happens, we will come back and clean this up a bit.

Conclusion

Ultimately, we didn’t hit our goal to get this working in a week. However, we will continue to work on this so our world can be saved (if only it were that easy)! If you have any tips or thoughts on this, please comment below! I’d love to hear about your solutions or for you to share your experience if you’ve done something similar.

Hello world!

So, after much consideration, I’ve retired from computer repair and decided to re-launch a site to focus more on my passion: developing solutions to interesting problems. My goal is to document my forays into the unknown (to me at least) for later reflection. Maybe it’ll help someone facing similar issues.

I have a few projects lined up and I’m considering resurrecting older projects to make them more modern. I’m currently working on a service for No Man’s Sky that allows a user to determine what they can build given resources they have (or could gather). More on this in another post.

I recently completed a Discord bot for Tom Clancy’s: The Division that leverages the very nice and clean APIs at Ruben Alamina’s site to easily search all the vendors when the reset their inventories. This too, I plan on writing up in a later post.

So, this site is running (finally) and I plan on writing up how I made it all automated, repeatable, version controlled, secured and backed up. I haven’t worked much with Docker before, but I do understand the concepts. This site runs on Docker! It’s in it’s own little container with a mounted Docker volume for plugin files. It’s also fronted by Nginx or, more specifically, an nginx-proxy container with a Let’s Encrypt companion container letsencrypt-nginx-companion (go ahead, check the SSL certificate) that auto-magically creates, renews, and configures any new publicly exposed containers with their own SSL certificates provided by Lets Encrypt. The site is backed by a MariaDB Docker container with a mounted data Docker volume. The website volume is read-only mounted to a WordPress Backup container that is also linked to the MariaDB container. This WordPress Backup container periodically copies and compresses the WordPress files in the mounted volume and creates a database dump of the WordPress database. These are both stored on the mounted backup volume. This backup volume also happens to be mounted to a Dropbox container and all of these backup files go straight to Dropbox! So, if this server goes kaboom! All i have to do is relaunch the containers on another host and restore the backups from Dropbox!

That’s it for the teaser. It was a fun project and I’ll definitely be diving into the details when I get back from GopherCon next week. I’m sure I’ll have some goodies from that as well (the No Man’s Sky and the Division projects are both written in Go).

I hope you enjoy the project write-ups I’ll be adding here. It’s not all programming, I’m developing a game with my son. I do plan on writing up other things as well (we have a Raspberry Pi and home automation is something I’ve dabbled in a wee bit).

What are you interested in? What projects would you like to see? I’d love to hear about them so leave a note in the comments below!