Your First Service
Scope: the whole journey in one chapter — a shell
script becomes a supervised, auto-restarting,
boot-enabled service. Every step run for real; the files
ship in examples/.
The program
examples/mere-demo.sh — a
heartbeat logger standing in for "your app":
#!/bin/bash
n=0
trap 'echo "mere-demo: shutting down cleanly (pid $$)"; exit 0' TERM
echo "mere-demo: started (pid $$)"
while true; do
n=$((n+1))
echo "mere-demo: heartbeat $n (pid $$)"
sleep 2 &
wait $!
done
Two details are the entire services contract in
miniature: it runs in the foreground (no daemonizing,
no & at the top level — systemd supervises the process
itself), and it treats SIGTERM as "shut down" —
which is what systemctl stop sends.
Install it where services can execute it:
sudo cp mere-demo.sh /usr/local/bin/ && sudo chmod +x /usr/local/bin/mere-demo.sh
The unit
examples/mere-demo.service
goes to /etc/systemd/system/. (Its anatomy is
chapter 2; here we run it.)
sudo cp mere-demo.service /etc/systemd/system/
systemd-analyze verify /etc/systemd/system/mere-demo.service
sudo systemctl daemon-reload
Verify printed nothing: good. Now:
sudo systemctl start mere-demo.service
systemctl is-active mere-demo.service
active
See it working
The heartbeats are in the journal, because the script's stdout is the service's log:
journalctl -u mere-demo.service -n 4
Oct 16 18:31:27 htch systemd[1]: Started mere-demo.service - Mere Mortals demo heartbeat service.
Oct 16 18:31:27 htch mere-demo.sh[16299]: mere-demo: started (pid 16299)
Oct 16 18:31:27 htch mere-demo.sh[16299]: mere-demo: heartbeat 1 (pid 16299)
Oct 16 18:31:29 htch mere-demo.sh[16299]: mere-demo: heartbeat 2 (pid 16299)
No log file was configured, opened, or rotated. Output went to stdout; systemd captured it. This is the journal chapter's whole thesis — Chapter 5.
Prove the supervision
Kill the main process outright — simulating a crash:
sudo kill -9 $(systemctl show -p MainPID --value mere-demo.service)
sleep 5
systemctl show -p MainPID --value mere-demo.service
The PID is different, and the journal explains:
systemd[1]: mere-demo.service: Scheduled restart job, restart counter is at 1.
systemd[1]: Started mere-demo.service - Mere Mortals demo heartbeat service.
That's Restart=on-failure + RestartSec=2 earning
their lines: crash detected, two-second pause, back up,
counter incremented (the counter is what start limits
use to give up on crash loops — Chapter 6).
Make it permanent, then stop it
sudo systemctl enable mere-demo.service # at boot, from now on
sudo systemctl stop mere-demo.service # stop now
journalctl -u mere-demo.service -n 3
systemd[1]: Stopping mere-demo.service ...
systemd[1]: mere-demo.service: Deactivated successfully.
systemd[1]: Stopped mere-demo.service ...
The trap in the script caught the SIGTERM and logged its
clean-shutdown line on the way out — check the journal
above the Stopped lines. Your app should do the same:
stop is a polite request first, and only becomes a kill
after TimeoutStopSec (default 90s on most distros)
expires.
You now own the complete lifecycle. Everything after this chapter is depth: types, timers, ordering, sandboxing.