27/Jul/2025

so i decided to host a small java minecraft server, should be easy right?

so let's start with containers i don't use docker, i don't like it, so i use lxc

the host is an arch system, which comes with lxc-net, a script that automatically sets up a bridge, iptables (or nftables if you have it, which you do because it requires dnsmasq), dnsmasq for dhcp and that's about it

setting up the container was simple enough, i used alpine, just editing subuid and subgid was enough to get it running unprivileged

the container instantly got internet access with no more setup, but had a dynamic ip and couldn't get any incoming new connections

making dnsmasq give it a static ip based on hostname worked, but sometimes it gets stuck as leased and gets a dynamic one :\

getting it to receive connections was annoying, iptables is annoying, nftables was so much better (and iptables apparently is backed by nftables on arch if you have it installed!)

here's the rule i got eventually:

table ip papermc { chain prerouting { type nat hook prerouting priority dstnat; policy accept; tcp dport 25565 dnat to 10.0.3.100 } }

now for actually starting the server, it can receive commands from the server operator in-game, RCON or the java console. i picked putting the java console inside tmux because i had no idea RCON existed lol

so the server is running under its own user (why would you run minecraft as root?) but i use root, since that's what i get when attaching to the container

solution? tmux sockets! normally they're owned by the user (and tmux enforces that, i think), but root can use them anyway (i hope you weren't relying on that for privacy and security)

so for actually sending commands, i write them with tmux send "$@" Enter. basic stuff, but what about reading back the output?

tmux pipe-pane -o "cat >> $PIPE" tmux send "$@" Enter sleep 0.5 tmux pipe-pane sed '1d,$d' $PIPE rm $PIPE

yikes.

for those those that don't know what pipe-pane does, it writes the changes from a pane (terminal view) onto a file. yeah, gotta be careful grepping that, the server can output anything while you're reading the pipe, or the command takes more than 0.5 seconds!

that horror aside, now the server can stay on without any players and eat resources, what do? after snapshot 24w33a, minecraft servers got the pause-when-empty-seconds property, which only pauses gameplay, bummer. need a new solution.

so, first step is stopping the server once nobody has been off for long enough

there are probably bukkit / paper plugins for that, but i didn't bother, i have the power of tmux!

using the earlier reading-output-from-command shell script, i can send list and get a list of players, but even more useful it writes There are N of a max of N players!

using that periodically, what to do to wake up? well, i tried socat, so when a player connected, the connection got closed and the java server starts back up, which worked but... the server appears offline! and there is no indication of it starting back up! so what exists to act like a minecraft server?

well there is mcsleepingserverstarter, but javascript... i would rather have a java server running

there's also lazymc, it's in rust but it's an entire proxy, overkill

so time to write my own!

sleepy is my own attempt at a sleep minecraft server!

the details are kinda boring, it's most just parsing minecraft java protocol datatypes, replying to status and ping requests and kicking on join

the most interesting part is that it prints on first join so you can make a shell script that starts the java server like so

nft destroy table ip papermc sleepy "0.0.0.0:25565" | while read -r line; do echo "$line" start_java_server sleep 15 nft add table ip papermc nft add chain ip papermc prerouting "{ type nat hook prerouting priority filter; policy accept; }" nft add rule ip papermc prerouting tcp dport 25565 redirect to 25566 pkill sleepy break done &

it changes nftables rules on the fly, mostly because minecraft binds to the port early before actually being able to reply, so it will look offline for a bit, so i just make it use another port and redirect traffic there after it's done

also credits to the minecraft wiki for their awesome protocol docs!