A game server that dies at three in the morning and stays dead until someone notices is a solved problem, and most people solve it with a screen session and a cron job that checks whether the process is alive. That works until the day it does not, which is usually the day the server is crashing in a loop and the cron job is dutifully restarting it into the same crash forever.
systemd already does this properly. The unit file is about fifteen lines, and four of them are the ones that matter.
The minimum that works
[Unit]
Description=Minecraft server
After=network-online.target
[Service]
Type=simple
User=minecraft
WorkingDirectory=/srv/minecraft
ExecStart=/usr/bin/java @flags.txt -jar paper.jar --nogui
Restart=on-failure
RestartSec=15s
[Install]
WantedBy=multi-user.target
That is a working unit. What follows is why each of the interesting lines is what it is.
Restart= — the line people get wrong
systemd.service(5) defines several values here and the two that matter are always and on-failure.
on-failure restarts when the process exits non-zero or is killed by a signal. It does not restart after a clean exit, which is exactly right, because a clean exit is what happens when you run stop from the console. With Restart=always, stopping your server from inside the game starts it again, and you discover this while trying to take it down for maintenance.
Use on-failure. Use always only when you genuinely want the process up no matter how it ended, and know that you have given up the ability to stop it from inside.
RestartSec= — the line people omit
Without it, systemd waits 100ms. A server that crashes on boot (a corrupt world, a plugin that throws during load, a port already bound) will therefore crash ten times a second.
systemd has a backstop for this: StartLimitIntervalSec and StartLimitBurst will eventually give up and leave the unit failed, which is better than an infinite loop but still means dozens of restarts first. Setting RestartSec=15s turns a crash loop into something you can read in the log while it happens, and gives a port held by the previous process time to be released.
Fifteen seconds is also roughly the difference between a log you can diagnose and a log that is just the same stack trace forty times, which matters when you get to working out what actually crashed.
Type= — the line that decides what "started" means
Type=simple tells systemd the process is up the moment it is forked. For a Java server that takes ninety seconds to load a world, that is a lie systemd believes: the unit reports active while the server is still reading region files.
It is usually the right lie. The alternative, Type=notify, requires the program to tell systemd when it is genuinely ready, and no game server does. What it means in practice is that systemctl start returning does not mean players can connect, and any deploy script that assumes otherwise will race.
The user, and why not root
User= runs the service as an unprivileged account. Game servers execute plugin code written by strangers; that code should not be able to read the rest of the box. This is the cheapest item on the whole server security list and the one most often skipped, because running as root is what the tutorial did.
systemd.exec(5) documents a set of hardening options worth reading once: PrivateTmp, ProtectSystem, NoNewPrivileges. Adding NoNewPrivileges=true and ProtectSystem=full costs nothing and closes a real class of escalation.
The out-of-memory killer, which is not your fault
When the kernel runs out of memory it picks a process to kill, and it picks the biggest one, which on a game server box is always the game server. The unit restarts it, the memory fills again, and it repeats.
systemd will show this: the log says the process was killed by signal 9 and MemoryMax was never hit, because the kernel acted before any cgroup limit did. The fix is not in the unit file. It is either less heap, more RAM, or fewer things sharing the box, and it lives in the same territory as what the hardware actually needs.
journalctl is now your log
Once the server runs under systemd, its console output goes to the journal rather than to a file you chose, and people spend an afternoon looking for a log that no longer exists.
journalctl -u yourserver -f follows it. -n 200 gives the last two hundred lines, --since "1 hour ago" bounds it by time, and -p err filters to errors. That is most of what reading a game server log needs.
The journal also rotates itself, which is worth knowing because it is one fewer thing to configure and one more thing that silently discards your history: the default keeps a percentage of the disk rather than a duration, so a chatty server may only retain a few days. SystemMaxUse= in journald.conf sets it explicitly, and setting it explicitly is better than finding out during an incident where the evidence was the first casualty.
The failure this does not survive
A unit that restarts correctly still restarts a server that is broken.
If the world is corrupt, or a plugin throws on load, or a port is already bound, systemd will restart it, it will fail again, and StartLimitBurst will eventually stop trying. That is the design working, and the machine is now down until a person looks.
Which means the unit file is only half the arrangement. Something has to tell you that the service entered a failed state, and systemd will do that itself with an OnFailure= line pointing at a one-shot unit that sends a message. Without it you have a server that recovers from everything it can recover from and stays down silently for everything else, which is worse than no automation at all because it removes the symptom that used to alert you.
What you get for the fifteen lines
journalctl -u minecraft -f instead of reattaching to a screen session. systemctl enable instead of an @reboot cron line. A restart policy that distinguishes a crash from a deliberate shutdown. And a service that comes back after the host reboots without anybody being awake for it.
The same unit works for any long-running server process (Source dedicated servers, FiveM, Rust) with only ExecStart and WorkingDirectory changing. If you are running several on one machine, that uniformity is worth more than any individual line, and it pairs with running them in containers rather than competing with it.
- Tags
- linuxoperationshosting
- Share
Published · 5 min read