Hacktoberfest is upon us! This month I’m hacking on small projects each week and sharing them.
Minecraft
This week my son and I hacked together an auto-deployed Docker based Minecraft server that is also automatically backed up and uses Git for managing server configurations. We haven’t finished the auto-backup to Dropbox portion yet, but that’s something we can always work on later! Here’s what we did…
Continuous Integration & Continuous Deployment
I knew I wanted this server to be a “set it and forget it” type of server. If anything went wrong, restarting it would fix it without losing world data. With this in mind, my first thought was using GitLab‘s CI/CD process and a .gitlab-ci.yml file. Looking through the Docker registry, I found a Minecraft server image that appears to stay up to date: itzg/minecraft-server. We simply used that and mounted a few volumes for version controlled configs and the world data directory. The GitLab Runner file is a lot to take in if this is the first time seeing this type of file. I’ll walk through each line and explain what it does. Here’s it is:
cache: key: "$CI_REGISTRY_IMAGE-$CI_COMMIT_SHA" untracked: true stages: - deploy deploy-prod: stage: deploy image: docker:latest only: - "master" variables: DOCKER_DRIVER: "overlay2" script: - docker pull itzg/minecraft-server:latest - docker stop minecraft || true - docker rm minecraft || true - docker run -d --name minecraft -p 25565:25565 -v minecraft-world:/data/world -v minecraft-config:/config -v minecraft-mods:/mods -v minecraft-plugins:/plugins -e EULA=TRUE --restart always itzg/minecraft-server - docker cp ./config/* minecraft:/config/ || true - docker cp ./data/* minecraft:/data/ || true - docker cp ./mods/* minecraft:/mods/ || true - docker cp ./plugins/* minecraft:/plugins/ || true - docker restart minecraft || true - docker pull janeczku/dropbox:latest - docker stop -t 0 minecraft-backup || true - docker rm minecraft-backup || true - docker run -d --restart=always --name=minecraft-backup -v minecraft-world:/dbox/Dropbox/minecraft-server/data -v minecraft-config:/dbox/Dropbox/minecraft-server/config -v minecraft-mods:/dbox/Dropbox/minecraft-server/mods -v minecraft-plugins:/dbox/Dropbox/minecraft-server/plugins janeczku/dropbox tags: - dockerDevil is in the Details
cache: key: "$CI_REGISTRY_IMAGE-$CI_COMMIT_SHA" untracked: trueLines 1-3 relate to GitLab and Docker Container caching. This will cache the resulting image to the GitLab Runner cache with the key on line 2. This is useful for downstream builds. I copied this from another project (Hacktoberfest!) so, I’m not sure if they are needed since this container isn’t used by any other job.
stages: - deployLine 4 defines the stages that will be used during this pipeline and line 5 is that stage. We define only the “deploy” stage as we aren’t performing any testing or other delivery stages. This is useful to define for organization within this file and allows you to introduce pipelines later. Again, this may not be necessary since it’s not involved in more than one pipeline. I did copy this from another project to reduce the amount of time spend recreating it so… Hacktoberfest!
deploy-prod: stage: deploy image: docker:latest only: - "master" variables: DOCKER_DRIVER: "overlay2" script: - docker pull itzg/minecraft-server:latest - docker stop minecraft || true - docker rm minecraft || true - docker run -d --name minecraft -p 25565:25565 -v minecraft-world:/data/world -v minecraft-config:/config -v minecraft-mods:/mods -v minecraft-plugins:/plugins -e EULA=TRUE --restart always itzg/minecraft-server - docker cp ./config/* minecraft:/config/ || true - docker cp ./data/* minecraft:/data/ || true - docker cp ./mods/* minecraft:/mods/ || true - docker cp ./plugins/* minecraft:/plugins/ || true - docker restart minecraft || true - docker pull janeczku/dropbox:latest - docker stop -t 0 minecraft-backup || true - docker rm minecraft-backup || true - docker run -d --restart=always --name=minecraft-backup -v minecraft-world:/dbox/Dropbox/minecraft-server/data -v minecraft-config:/dbox/Dropbox/minecraft-server/config -v minecraft-mods:/dbox/Dropbox/minecraft-server/mods -v minecraft-plugins:/dbox/Dropbox/minecraft-server/plugins janeczku/dropboxLine 6 defines a new job called
deploy-prod
and is only deployed during thedeploy
stage we just defined and only for the master branch (line 9 and 10). This will spin up a new docker container using the latest docker image (line 8) from the Docker registry. Once spun up line 11 defines environmental variables available to the container and line 12 sets theDOCKER_DRIVER
. This driver is supposed to be more efficient. Again, this was copied from another project and I haven’t had any problems, so I leave it alone. Lines 13-26 are the meat and potatoes. The script section does the heavy lifting.script: - docker pull itzg/minecraft-server:latest - docker stop minecraft || true - docker rm minecraft || true - docker run -d --name minecraft -p 25565:25565 -v minecraft-world:/data/world -v minecraft-config:/config -v minecraft-mods:/mods -v minecraft-plugins:/plugins -e EULA=TRUE --restart always itzg/minecraft-server - docker cp ./config/* minecraft:/config/ || true - docker cp ./data/* minecraft:/data/ || true - docker cp ./mods/* minecraft:/mods/ || true - docker cp ./plugins/* minecraft:/plugins/ || true - docker restart minecraft || true - docker pull janeczku/dropbox:latest - docker stop -t 0 minecraft-backup || true - docker rm minecraft-backup || true - docker run -d --restart=always --name=minecraft-backup -v minecraft-world:/dbox/Dropbox/minecraft-server/data -v minecraft-config:/dbox/Dropbox/minecraft-server/config -v minecraft-mods:/dbox/Dropbox/minecraft-server/mods -v minecraft-plugins:/dbox/Dropbox/minecraft-server/plugins janeczku/dropboxLine 13 defines what will run on the GitLab Runner. This is the reason we use the latest docker image on line 8. The commands on lines 14 through 26 will actually leverage Docker on the GitLab Runner and manipulate docker containers. We start off on line 14 by pulling the itzg/minecraft-server image (in case Minecraft server releases an update). This updates the image that Docker can use but doesn’t update the running container. After we pull the latest container image, we stop (line 15) and remove (line 16) the current running Minecraft server container. The
|| true
guarantees the execution will not return an error which would stop this from succeeding in the event the container isn’t running or doesn’t exist. Line 15 doesn’t have a timeout for forcing the container to shutdown so it will wait to clean up and prepare the world for the server going down. This helps prevent data corruption. Line 16 removes the running container so we can re-deploy with the latest version.- docker run -d --name minecraft -p 25565:25565 -v minecraft-world:/data/world -v minecraft-config:/config -v minecraft-mods:/mods -v minecraft-plugins:/plugins -e EULA=TRUE --restart always itzg/minecraft-serverLine 17 does a lot. It runs a container named Minecraft (
--name minecraft
) as a daemon service (-d
) and binds the host port 25565 to the container port 25565 (-p 25565:25565
). Since Minecraft clients check for this specific port, I opted to just bind the public exposed port to the same container port. We then mount several Docker volumes with the-v
flag. I like to think of a Docker Volume as a flash drive. It’s just a storage device you can plug into other containers. The only difference is that you can plug it into multiple containers simultaneously and you can mount it to a specific folder. Here, we mounted theminecraft-world
volume to the/data/world
container directory. We also mount theminecraft-config
to/config
,minecraft-mods
to/mods
andminecraft-plugins
to/plugins
. Once these mounts are in place, we set the containerEULA
environment variable totrue
, set the container to always restart if it goes down, and finally tell Docker what image to use for the container. This runs the actual container and the whole server spins up performing any startup tasks before the server is live and available for connection!- docker cp ./config/* minecraft:/config/ || true - docker cp ./data/* minecraft:/data/ || true - docker cp ./mods/* minecraft:/mods/ || true - docker cp ./plugins/* minecraft:/plugins/ || true - docker restart minecraft || trueOnce this container is running, we copy version controlled files (if any) from the checked out repository into the mounted volumes on the Minecraft server container (lines 18-21). This lets us version control configurations, plugins, and mods and auto-deploy them to the server. Once these are in place, we restart the container one more time (line 22) for these to take effect. These files are copied in this way so we don’t have to worry if this is the first time the container is ran. If we mounted the files before starting the container, the container wouldn’t exist on the first run and would require 2 deployments for these changes to take effect.
- docker pull janeczku/dropbox:latest - docker stop -t 0 minecraft-backup || true - docker rm minecraft-backup || true - docker run -d --restart=always --name=minecraft-backup -v minecraft-world:/dbox/Dropbox/minecraft-server/data -v minecraft-config:/dbox/Dropbox/minecraft-server/config -v minecraft-mods:/dbox/Dropbox/minecraft-server/mods -v minecraft-plugins:/dbox/Dropbox/minecraft-server/plugins janeczku/dropboxLines 23-26 are backup related. Line 23 updates the local version of the janeczku/dropbox:latest image. Lines 24 and 25 should look familiar. These stop and remove the existing container while guaranteeing success if the container is already stopped or doesn’t exist. Line 26 should also look familiar. Here, we start another container as a daemon (
-d
) that restarts whenever it stops (--restart=always
) namedminecraft-backup
(--name=minecraft-backup
) with a few volumes mounted to the container. These volume mounts should also look familiar! We mount the same volumes here as we do the Minecraft server so this container can periodically back up the contents of the volumes to Dropbox. We are still troubleshooting why this isn’t quite working and hope to have this resolved next time.tags: - dockerFinally, likes 27 and 28 tell GitLab what GitLab Runner to run this job on. Each GitLab Runner may have tags that jobs can use to run on specific runners. This particular job requires a GitLab Runner that is Docker enabled. This doesn’t actually mean there is a Docker enabled GitLab Runner available, but in my case, I have already set up a Docker enabled GitLab Runner and I can force jobs to use it by adding this tag to this file.
That’s it! Now we have a Minecraft server! Even without the Dropbox backups, this project was fun. It was an awesome moment when my son wanted to stand up a Minecraft server and actually stayed interested through the build. He got to see behind the scenes how the game is set up, although I don’t think he fully understands how it all works, but maybe one day!
We will continue tinkering with this script as we add configuration files and get the Dropbox backups working. I will be open sourcing this when we have completed working on it. Until then feel free to use this as a template for your next Minecraft Server and I will be updating this post as I update the files.