One of the few things I know about Minecraft is, some players I know and love could be playing together if someone were to run a server for them. That’s the sort of thing I’d gladly do, provided I could approximately never again pay attention to it.

Here’s what I came up with. Let’s see how it pans out.

Prerequisites

My Virtual Private Server at Panix is NetBSD/amd64 with plenty of RAM, disk, and network headroom. Marginal additional usage is therefore free.

System- and pkgsrc-provided services are controlled by the usual NetBSD-style rc.d scripts.

Site-specific services are supervised, including supervision trees controlled by each user. In the following excerpt from my running system:

/service/sniproxy: up (pid 11133) 81141 seconds

/service/svscan-notqmail: up (pid 325) 846389 seconds
  /home/notqmail/service/renewssl.www.notqmail.org: up (pid 20951) 81141 seconds
  /home/notqmail/service/www.notqmail.org: up (pid 24539) 81141 seconds

/service/svscan-schmonz: up (pid 394) 846389 seconds
  /home/schmonz/service/agilein3minut.es: up (pid 26325) 81141 seconds
  /home/schmonz/service/latentagility.com: up (pid 24554) 81141 seconds
  /home/schmonz/service/renewssl.agilein3minut.es: up (pid 18299) 81141 seconds
  /home/schmonz/service/renewssl.latentagility.com: up (pid 14394) 81141 seconds
  /home/schmonz/service/renewssl.schmonz.com: up (pid 12424) 81140 seconds
  /home/schmonz/service/schmonz.com: up (pid 21449) 81141 seconds

These packages were already installed:

  • djbdns-run
  • s6-portable-utils
  • unzip
  • curl
  • py-html2text
  • jq

I’ve added these:

  • openjdk21
  • execline

I’m installing Minecraft’s server software manually, so it’ll be my job to notice when to update. Gotta automate the noticing, at least.

Oh, and I need a sensible process-supervision run script for the Minecraft server. The script needs to solve two application-specific system-integration problems:

  1. The usual supervision signals cause the server to terminate without saving the state of the world, which seems… rude
  2. The usual way to configure a running server is to get on the console and type commands into it, which implies having started it inside a tmux session (which, I love tmux, but not for this)

We solve for (1) by running the server as a child process and translating supervision signals into Minecraft commands.

We solve for (2) by connecting a named pipe to the server’s standard input. Then commands can be sent to the server using nothing but echo and ordinary output redirection.

Without even really being asked, my brain instantly produced the mechanisms for (1) in Perl. Then someone on #s6 shared a run script that additionally solved (2) while being more than twice as short as mine. How? By writing it in execline, a language expressly designed to be this kind of glue.

Step by step: Supervising Minecraft service

1. Create dedicated Unix user with its own process supervisor

useradd -m minecraft
mkdir -p /etc/service/svscan-minecraft/log
cd /etc/service/svscan-minecraft
cat > log/run <<'EOF'
#!/bin/sh
exec setuidgid minecraft logger -t svscan-minecraft -p daemon.info
EOF
chmod +x log/run
cat > run <<'EOF'
#!/bin/sh
exec 2>&1
exec setuidgid minecraft argv0 svscan svscan-minecraft /home/minecraft/service
EOF
chmod +x run
ln -s /etc/service/svscan-minecraft /var/service/

2. Globally, map hostname to IP

echo '+minecraft.schmonz.com:my.server.ip.here' >> /etc/tinydns/data
service tinydns reload

3. Locally, map port number to service name

echo 'minecraft 25565/tcp # minecraft.schmonz.com' >> /etc/services
service sysdb services

4. Allow incoming connections

$EDITOR /etc/npf.conf # add minecraft to $services_tcp
service npf reload

5. Download software

su minecraft
cd
mkdir -p sites/schmonz.com/minecraft/service
cd sites/schmonz.com/minecraft
mkdir bin
curl -o bin/server.jar https://piston-data.mojang.com/v1/objects/e6ec2f64e6080b9b5d9b471b291c33cc7f509733/server.jar

6. Accept EULA

mkdir data
echo 'eula=true' > data/eula.txt

7. Create control socket

mkfifo -m 0600 data/control

8. Symlink service logs where I usually look

ln -s data/logs logs

9. Create run script

cat > service/run <<'EOF'
#!/opt/pkg/bin/execlineb -P
fdmove -c 2 1
cd /home/minecraft/sites/schmonz.com/minecraft/data
redirfd -w -nb 3 control
trap -x
{
    SIGINT  { fdmove 1 3 s6-echo stop }
    SIGHUP  { fdmove 1 3 s6-echo stop }
    SIGTERM { fdmove 1 3 s6-echo stop }
    SIGPIPE { fdmove 1 3 s6-echo stop }
}
fdclose 3
redirfd -r 0 control
java -Xmx1024M -Xms1024M -jar ../bin/server.jar --nogui
EOF
chmod +x service/run

10. Enable service

ln -s /home/minecraft/sites/schmonz.com/minecraft/service ~/service/minecraft.schmonz.com

11. Send initial configuration

cat > data/control <<'EOF'
whitelist on
whitelist add so-and-so
save-on
EOF

Be notified of updates

As the minecraft user:

1. Create service and run script

cd ~/sites/schmonz.com/minecraft
mkdir -p update/service/log
cd update/service

cat > log/run <<'EOF'
#!/bin/sh

exec multilog t ./main
EOF
chmod +x log/run

cat > run <<'EOF'
#!/bin/sh

set -e
set -x

exec 2>&1

MINECRAFT_SERVER_DOWNLOAD_PAGE='https://www.minecraft.net/en-us/download/server'

running_version() {
   unzip -p ../bin/server.jar version.json \
       | jq -r .name
}

available_version() {
   curl \
       --silent \
       --user-agent "Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/81.0" "${MINECRAFT_SERVER_DOWNLOAD_PAGE}" \
       | html2text \
       | grep -i 'minecraft_server.* nogui' \
       | sed -e 's|.*minecraft_server\.||' -e 's|\.jar.*||'
}

main() {
   cd /home/minecraft/sites/schmonz.com/minecraft/data

   while true; do
       running="$(running_version)"
       available="$(available_version)"
       if [ "${running}" != "${available}" ]; then
           echo "MINECRAFT SERVER UPDATE NEEDED: ${running} -> ${available}"
           echo "" | mail -s "Please update Minecraft server: foo -> bar" your@email.address.here
       fi
       sleep 250000
   done
}

main "$@"
exit $?
EOF
chmod +x run

2. Enable service

ln -s /home/minecraft/sites/schmonz.com/minecraft/update/service ~/service/update.minecraft.schmonz.com

Further improvements

If it’s desirable to have the server periodically auto-save periodically, I’ll add one more service that writes save-all to data/control.

If pkgsrc had a minecraft-server package (FreeBSD Ports does), I could delete my ad hoc update-notification service. Maybe I’ll make it so. I do like packaging things as part of my area-under-the-curve strategy.

My host-specific supervision trees and pkgsrc’s djbware rc.d scripts (qmailsmtpd, for instance) are still using daemontools. I’ve had the intention to switch to s6 for a long time. When it happens, I’ll be happy about it.

I haven’t tried to understand why there are “Java” and “Bedrock” editions of Minecraft, but it seems like they don’t interoperate. I’m sure there are reasons for this product segmentation and that I’ll come to better understand them in the fullness of time. Meanwhile, if and when I need to add Bedrock client interop, Geyser looks like an easy option — once I’m running something other than the vanilla upstream Minecraft Java server. I’ll need to try to understand that whole situation, too.

Are you an experienced Minecraft server administrator? Please share tips and advice!