Starting a server by hand in an SSH session works right up until the session closes or the box reboots. A systemd unit fixes that properly: the server starts on boot, restarts if it crashes, runs as an unprivileged user, and its console goes to the journal where you can actually read it. This is the setup that turns a test server into something you can leave running.
1. Decide who restarts on crash
There is one gotcha unique to HLDS. The usual launcher, hlds_run, is a shell wrapper that already restarts hlds_linux after a crash. If systemd also restarts on failure, the two can fight — systemd sees the wrapper still alive and never triggers, or you get confusing double restarts. Pick one:
- Let systemd restart (cleaner): pass
-norestarttohlds_runso it exits on crash and systemd brings it back. This is the approach below. - Let hlds_run restart: use
Restart=noand rely on the wrapper. You then lose systemd's crash accounting.
2. Write the unit
Create /etc/systemd/system/hlds.service:
[Unit] Description=CS 1.6 Dedicated Server After=network-online.target Wants=network-online.target [Service] Type=simple User=steam Group=steam WorkingDirectory=/home/steam/hlds ExecStart=/home/steam/hlds/hlds_run -game cstrike -norestart \ -strictportbind -ip 0.0.0.0 -port 27015 \ +map de_dust2 +maxplayers 20 +sv_lan 0 Restart=on-failure RestartSec=5 KillSignal=SIGINT [Install] WantedBy=multi-user.target
Points that matter:
User=steam— never run the engine as root. If the unit is under/etc/systemd/systemit runs as root unless you set the user.WorkingDirectory— HLDS resolvescstrike/relative to the working directory. Get this wrong and it dies with a missing-liblist.gamstyle error.-norestart— hands crash-restart to systemd (see step 1).KillSignal=SIGINT— lets HLDS shut down cleanly onsystemctl stopinstead of being hard-killed.network-online.target— the server needs the network up before it heartbeats the master.
3. Enable and start
sudo systemctl daemon-reload sudo systemctl enable --now hlds sudo systemctl status hlds
enable makes it start on boot; --now starts it immediately. status should show active (running) and the most recent console lines.
4. Read the console
Because the unit is Type=simple, everything HLDS prints goes to the journal:
journalctl -u hlds -f
That is your live console. To see the last boot's output:
journalctl -u hlds -n 100 --no-pager
5. Sending commands to a running server
systemd captures stdout but does not give you an interactive stdin. To type commands into a running server you have two clean options: use RCON from a client or web tool, or run the server inside a tmux/screen session that systemd launches. If you want console access, wrap the start in tmux:
ExecStart=/usr/bin/tmux new-session -d -s cs16 \ '/home/steam/hlds/hlds_run -game cstrike -norestart -port 27015 +map de_dust2 +maxplayers 20 +sv_lan 0' ExecStop=/usr/bin/tmux kill-session -t cs16 Type=forking RemainAfterExit=yes
Then attach with tmux attach -t cs16. For most people, RCON is enough and the plain unit above is better because systemd tracks the process directly.
6. A template for many servers
Running several instances? Use a template unit [email protected] parameterised by directory, as covered in running multiple servers on one VPS, and enable hlds@server1, hlds@server2, and so on.
Common errors
- Unit runs as root — you forgot
User=steam. Add it; a network binary running as root is a real risk. status=203/EXECor "No such file or directory" — theExecStartpath is wrong, orhlds_runis not executable. Use absolute paths andchmod +x.- Server can't find cstrike /
liblist.gam—WorkingDirectoryis wrong. It must be the server root that containshlds_runandcstrike/. - Double restarts on crash — both systemd and
hlds_runare restarting. Add-norestartto the command. - Starts before the network, never lists — add
After=network-online.targetandWants=network-online.target. systemctl stoptakes 90s then kills it — addKillSignal=SIGINTso HLDS shuts down cleanly instead of timing out.
Hardening the unit
Because systemd manages the process directly, you can sandbox it further at no cost. A few directives worth adding to the [Service] block on a public server:
NoNewPrivileges=true PrivateTmp=true ProtectSystem=full ProtectHome=read-only
ProtectHome=read-only is too strict if the server writes logs and bans under /home/steam — in that case drop it or switch to ReadWritePaths=/home/steam/hlds. Test each directive one at a time: an over-tight sandbox that blocks a path the engine needs shows up as a startup failure in journalctl, not as an obvious permission message.
Verification
sudo systemctl restart hlds sudo systemctl status hlds ss -lunp | grep 27015
Status should read active (running) and the port should be bound by hlds_linux. Now reboot the box and confirm the server comes back on its own:
sudo reboot # after it comes back: systemctl is-active hlds
An active result after a cold boot means autostart works. Combine this with an annotated server.cfg and the correct firewall ports and the server is production-ready.









