Skip to content

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.