Most systemd tutorials hand you a unit file to copy-paste without explaining what any of it does. That’s fine until you need to write your own for a script that doesn’t come with one — which is most of the time if you’re self-hosting anything custom.
The three sections you actually need
A minimal working unit file has three blocks. Here’s one that keeps an arbitrary script running and restarts it if it crashes:
[Unit]
Description=Heartbeat test service
After=network.target
[Service]
Type=simple
ExecStart=/home/you/scripts/heartbeat.sh
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
[Unit]— metadata and ordering.After=network.targetjust means “don’t start this before the network is up,” not a hard dependency — useRequires=alongsideAfter=if the service genuinely can’t function without something else running first.[Service]— the actual behavior.Type=simplemeans systemd considers the service started as soon as it executesExecStart— fine for a long-running foreground process like the example above.Restart=on-failureplusRestartSec=3means a crash gets retried after 3 seconds instead of staying dead.[Install]— what happens when youenableit.WantedBy=says which target pulls this service in at boot/login.
The gotcha that catches almost everyone once
If you’re writing a user service (one that runs as your own user, not system-wide —
placed in ~/.config/systemd/user/ and managed with systemctl --user), WantedBy=multi-user.target
looks correct because that’s what every system-service example uses. It isn’t correct here.
multi-user.target doesn’t exist in a user session — enabling against it produces:
Unit /home/alan/.config/systemd/user/heartbeat-test.service is added as a dependency
to a non-existent unit multi-user.target.
The service still technically works when started manually, but it won’t actually enable
correctly for auto-start. The fix, confirmed on a real system: use WantedBy=default.target
for user services instead. System-wide services (the ones in /etc/systemd/system/,
managed without --user) are the ones that correctly use multi-user.target.
Putting it into place
For a user service:
mkdir -p ~/.config/systemd/user
cp heartbeat.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now heartbeat.service
For a system-wide service, the same commands without --user, placing the file in
/etc/systemd/system/ instead, and prefixed with sudo.
Checking it actually worked
Two commands cover almost everything you need day to day:
systemctl --user status heartbeat.service # is it running, and since when
journalctl --user -u heartbeat.service -f # live logs, -f to follow
Drop --user for a system-wide service. journalctl -f in particular is worth knowing —
it’s the systemd equivalent of tail -f for a service’s output, and it’s usually the
fastest way to see why something didn’t start the way you expected.