| Title: Using systemd to make a Minecraft server to start on-demand and | |
| stop when it has no player | |
| Author: Solène | |
| Date: 20 August 2022 | |
| Tags: minecraft nixos systemd automation | |
| Description: This article explains how to use systemd to start a | |
| network daemon upon connection and make it stop when it's not needed | |
| anymore, using Minecraft as a real world example. | |
| # Introduction | |
| Sometimes it feels I have specific use cases I need to solve alone. | |
| Today, I wanted to have a local Minecraft server running on my own | |
| workstation, but only when someone needs it. The point was that | |
| instead of having a big java server running all the system, Minecraft | |
| server would start upon connection from a player, and would stop when | |
| no player remains. | |
| However, after looking a bit more into this topic, it seems I'm not the | |
| only one who need this. | |
| on-demand-minecraft: a project to automatically start a remote cloud server for… | |
| minecraft-server-hibernation: a wrapper that starts and stop a Minecraft server… | |
| As often, I prefer not to rely on third party tools when I can, so I | |
| found a solution to implement this using only systemd. | |
| Even better, note that this method can work with any daemon given you | |
| can programmatically get the information whether to let it running or | |
| stop. In this example, I'm using Minecraft and the server stop is | |
| decided based on the player connecting fetch through rcon (a remote | |
| administration protocol). | |
| # The setup | |
| I made a simple graph to show the dependencies, there are many systemd | |
| components used to build this. | |
| systemd dependency graph | |
| The important part is the use of the systemd proxifier, it's a command | |
| to accept a connection over TCP and relay it to another socket, | |
| meanwhile you can do things such as starting a server and wait for it | |
| to be ready. This is the key of this setup, without it, this couldn't | |
| be possible. | |
| Basically, listen-minecraft.socket listens on the public TCP port and | |
| runs listen-minecraft.service upon connection. This service needs | |
| hook-minecraft.service which is responsible for stopping or starting | |
| minecraft, but will also make listen-minecraft.service wait for the TCP | |
| port to be open so the proxifier will relay the connection to the | |
| daemon. | |
| Then, minecraft-server.service is started alongside with | |
| stop-minecraft.timer which will regularly run stop-minecraft.service to | |
| try to stop the server if possible. | |
| # Configuration | |
| I used NixOS to configure my on-demand Minecraft server. This is | |
| something you can do on any systemd capable system, but I will provide | |
| a NixOS example, it shouldn't be hard to translate to a regular systemd | |
| configuration files. | |
| ```nix | |
| { config, lib, pkgs, modulesPath, ... }: | |
| let | |
| # check every 20 seconds if the server | |
| # need to be stopped | |
| frequency-check-players = "*-*-* *:*:0/20"; | |
| # time in second before we could stop the server | |
| # this should let it time to spawn | |
| minimum-server-lifetime = 300; | |
| # minecraft port | |
| # used in a few places in the code | |
| # this is not the port that should be used publicly | |
| # don't need to open it on the firewall | |
| minecraft-port = 25564; | |
| # this is the port that will trigger the server start | |
| # and the one that should be used by players | |
| # you need to open it in the firewall | |
| public-port = 25565; | |
| # a rcon password used by the local systemd commands | |
| # to get information about the server such as the | |
| # player list | |
| # this will be stored plaintext in the store | |
| rcon-password = "260a368f55f4fb4fa"; | |
| # a script used by hook-minecraft.service | |
| # to start minecraft and the timer regularly | |
| # polling for stopping it | |
| start-mc = pkgs.writeShellScriptBin "start-mc" '' | |
| systemctl start minecraft-server.service | |
| systemctl start stop-minecraft.timer | |
| ''; | |
| # wait 60s for a TCP socket to be available | |
| # to wait in the proxifier | |
| # idea found in http://web.archive.org/web/20240215035104/https://blog.develo… | |
| wait-tcp = pkgs.writeShellScriptBin "wait-tcp" '' | |
| for i in `seq 60`; do | |
| if ${pkgs.libressl.nc}/bin/nc -z 127.0.0.1 ${toString minecraft-port} > /… | |
| exit 0 | |
| fi | |
| ${pkgs.busybox.out}/bin/sleep 1 | |
| done | |
| exit 1 | |
| ''; | |
| # script returning true if the server has to be shutdown | |
| # for minecraft, uses rcon to get the player list | |
| # skips the checks if the service started less than minimum-server-lifetime | |
| no-player-connected = pkgs.writeShellScriptBin "no-player-connected" '' | |
| servicestartsec=$(date -d "$(systemctl show --property=ActiveEnterTimestamp… | |
| serviceelapsedsec=$(( $(date +%s) - servicestartsec)) | |
| # exit if the server started less than 5 minutes ago | |
| if [ $serviceelapsedsec -lt ${toString minimum-server-lifetime} ] | |
| then | |
| echo "server is too young to be stopped" | |
| exit 1 | |
| fi | |
| PLAYERS=`printf "list\n" | ${pkgs.rcon.out}/bin/rcon -m -H 127.0.0.1 -p 255… | |
| if echo "$PLAYERS" | grep "are 0 of a" | |
| then | |
| exit 0 | |
| else | |
| exit 1 | |
| fi | |
| ''; | |
| in | |
| { | |
| # use NixOS module to declare your Minecraft | |
| # rcon is mandatory for no-player-connected | |
| services.minecraft-server = { | |
| enable = true; | |
| eula = true; | |
| openFirewall = false; | |
| declarative = true; | |
| serverProperties = { | |
| server-port = minecraft-port; | |
| difficulty = 3; | |
| gamemode = "survival"; | |
| force-gamemode = true; | |
| max-players = 10; | |
| level-seed = 238902389203; | |
| motd = "NixOS Minecraft server!"; | |
| white-list = false; | |
| enable-rcon = true; | |
| "rcon.password" = rcon-password; | |
| }; | |
| }; | |
| # don't start Minecraft on startup | |
| systemd.services.minecraft-server = { | |
| wantedBy = pkgs.lib.mkForce []; | |
| }; | |
| # this waits for incoming connection on public-port | |
| # and triggers listen-minecraft.service upon connection | |
| systemd.sockets.listen-minecraft = { | |
| enable = true; | |
| wantedBy = [ "sockets.target" ]; | |
| requires = [ "network.target" ]; | |
| listenStreams = [ "${toString public-port}" ]; | |
| }; | |
| # this is triggered by a connection on TCP port public-port | |
| # start hook-minecraft if not running yet and wait for it to return | |
| # then, proxify the TCP connection to the real Minecraft port on localhost | |
| systemd.services.listen-minecraft = { | |
| path = with pkgs; [ systemd ]; | |
| enable = true; | |
| requires = [ "hook-minecraft.service" "listen-minecraft.socket" ]; | |
| after = [ "hook-minecraft.service" "listen-minecraft.socket"]; | |
| serviceConfig.ExecStart = "${pkgs.systemd.out}/lib/systemd/systemd-socket-p… | |
| }; | |
| # this starts Minecraft is required | |
| # and wait for it to be available over TCP | |
| # to unlock listen-minecraft.service proxy | |
| systemd.services.hook-minecraft = { | |
| path = with pkgs; [ systemd libressl busybox ]; | |
| enable = true; | |
| serviceConfig = { | |
| ExecStartPost = "${wait-tcp.out}/bin/wait-tcp"; | |
| ExecStart = "${start-mc.out}/bin/start-mc"; | |
| }; | |
| }; | |
| # create a timer running every frequency-check-players | |
| # that runs stop-minecraft.service script on a regular | |
| # basis to check if the server needs to be stopped | |
| systemd.timers.stop-minecraft = { | |
| enable = true; | |
| timerConfig = { | |
| OnCalendar = "${frequency-check-players}"; | |
| Unit = "stop-minecraft.service"; | |
| }; | |
| wantedBy = [ "timers.target" ]; | |
| }; | |
| # run the script no-player-connected | |
| # and if it returns true, stop the minecraft-server | |
| # but also the timer and the hook-minecraft service | |
| # to prepare a working state ready to resume the | |
| # server again | |
| systemd.services.stop-minecraft = { | |
| enable = true; | |
| serviceConfig.Type = "oneshot"; | |
| script = '' | |
| if ${no-player-connected}/bin/no-player-connected | |
| then | |
| echo "stopping server" | |
| systemctl stop minecraft-server.service | |
| systemctl stop hook-minecraft.service | |
| systemctl stop stop-minecraft.timer | |
| fi | |
| ''; | |
| }; | |
| } | |
| ``` | |
| # Conclusion | |
| I'm really happy to have figured out this smart way to create an | |
| on-demand Minecraft, and the design can be reused with many other kinds | |
| of daemons. |