--- title: "How to Auto-Start HLDS with a systemd Service" excerpt: "Write a proper systemd unit for HLDS so it starts on boot, restarts on crash, runs as an unprivileged user, and streams its console to journalctl." category: "installation" tags: "systemd, autostart" image: "" read_time: "8 min read" ---

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:

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:

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 hlds@.service parameterised by directory, as covered in running multiple servers on one VPS, and enable hlds@server1, hlds@server2, and so on.

Common errors

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.