Promotiraj s'rv'r
98 lines 6.7 KiB Markdown Source Rendered Raw Permalink History
---
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"
---
<p>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.</p>

<h2>1. Decide who restarts on crash</h2>
<p>There is one gotcha unique to HLDS. The usual launcher, <code>hlds_run</code>, is a shell wrapper that already restarts <code>hlds_linux</code> after a crash. If systemd <em>also</em> restarts on failure, the two can fight — systemd sees the wrapper still alive and never triggers, or you get confusing double restarts. Pick one:</p>
<ul>
<li><strong>Let systemd restart</strong> (cleaner): pass <code>-norestart</code> to <code>hlds_run</code> so it exits on crash and systemd brings it back. This is the approach below.</li>
<li><strong>Let hlds_run restart</strong>: use <code>Restart=no</code> and rely on the wrapper. You then lose systemd's crash accounting.</li>
</ul>

<h2>2. Write the unit</h2>
<p>Create <code>/etc/systemd/system/hlds.service</code>:</p>
<pre>[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</pre>
<p>Points that matter:</p>
<ul>
<li><strong><code>User=steam</code></strong> — never run the engine as root. If the unit is under <code>/etc/systemd/system</code> it runs as root unless you set the user.</li>
<li><strong><code>WorkingDirectory</code></strong> — HLDS resolves <code>cstrike/</code> relative to the working directory. Get this wrong and it dies with a missing-<code>liblist.gam</code> style error.</li>
<li><strong><code>-norestart</code></strong> — hands crash-restart to systemd (see step 1).</li>
<li><strong><code>KillSignal=SIGINT</code></strong> — lets HLDS shut down cleanly on <code>systemctl stop</code> instead of being hard-killed.</li>
<li><strong><code>network-online.target</code></strong> — the server needs the network up before it heartbeats the master.</li>
</ul>

<h2>3. Enable and start</h2>
<pre>sudo systemctl daemon-reload
sudo systemctl enable --now hlds
sudo systemctl status hlds</pre>
<p><code>enable</code> makes it start on boot; <code>--now</code> starts it immediately. <code>status</code> should show <code>active (running)</code> and the most recent console lines.</p>

<h2>4. Read the console</h2>
<p>Because the unit is <code>Type=simple</code>, everything HLDS prints goes to the journal:</p>
<pre>journalctl -u hlds -f</pre>
<p>That is your live console. To see the last boot's output:</p>
<pre>journalctl -u hlds -n 100 --no-pager</pre>

<h2>5. Sending commands to a running server</h2>
<p>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 <code>tmux</code>/<code>screen</code> session that systemd launches. If you want console access, wrap the start in tmux:</p>
<pre>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</pre>
<p>Then attach with <code>tmux attach -t cs16</code>. For most people, RCON is enough and the plain unit above is better because systemd tracks the process directly.</p>

<h2>6. A template for many servers</h2>
<p>Running several instances? Use a template unit <code>[email protected]</code> parameterised by directory, as covered in <a href="/tutorials/multiple-cs16-servers-one-vps">running multiple servers on one VPS</a>, and enable <code>hlds@server1</code>, <code>hlds@server2</code>, and so on.</p>

<h2>Common errors</h2>
<ul>
<li><strong>Unit runs as root</strong> — you forgot <code>User=steam</code>. Add it; a network binary running as root is a real risk.</li>
<li><strong><code>status=203/EXEC</code> or "No such file or directory"</strong> — the <code>ExecStart</code> path is wrong, or <code>hlds_run</code> is not executable. Use absolute paths and <code>chmod +x</code>.</li>
<li><strong>Server can't find cstrike / <code>liblist.gam</code></strong> — <code>WorkingDirectory</code> is wrong. It must be the server root that contains <code>hlds_run</code> and <code>cstrike/</code>.</li>
<li><strong>Double restarts on crash</strong> — both systemd and <code>hlds_run</code> are restarting. Add <code>-norestart</code> to the command.</li>
<li><strong>Starts before the network, never lists</strong> — add <code>After=network-online.target</code> and <code>Wants=network-online.target</code>.</li>
<li><strong><code>systemctl stop</code> takes 90s then kills it</strong> — add <code>KillSignal=SIGINT</code> so HLDS shuts down cleanly instead of timing out.</li>
</ul>

<h2>Hardening the unit</h2>
<p>Because systemd manages the process directly, you can sandbox it further at no cost. A few directives worth adding to the <code>[Service]</code> block on a public server:</p>
<pre>NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=read-only</pre>
<p><code>ProtectHome=read-only</code> is too strict if the server writes logs and bans under <code>/home/steam</code> — in that case drop it or switch to <code>ReadWritePaths=/home/steam/hlds</code>. 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 <code>journalctl</code>, not as an obvious permission message.</p>

<h2>Verification</h2>
<pre>sudo systemctl restart hlds
sudo systemctl status hlds
ss -lunp | grep 27015</pre>
<p>Status should read <code>active (running)</code> and the port should be bound by <code>hlds_linux</code>. Now reboot the box and confirm the server comes back on its own:</p>
<pre>sudo reboot
# after it comes back:
systemctl is-active hlds</pre>
<p>An <code>active</code> result after a cold boot means autostart works. Combine this with an <a href="/tutorials/setup-server-cfg-cs16">annotated server.cfg</a> and the correct <a href="/tutorials/cs16-server-firewall-ports">firewall ports</a> and the server is production-ready.</p>