Why this matters
You have a small program that must keep running: after you log out, after it crashes, after a reboot. Running it by hand in your terminal is not enough - it dies the moment your SSH session ends. Turning it into a service hands the job to systemd. This lesson walks you through doing that for a tiny script.
What you need to know already: 2.1 (unit files and sections), 2.3 (why your files go in /etc/systemd/system), redirection and stdout (1.7).
Two words: script and executable
- A script is a text file full of shell commands, run top to bottom as if you had typed them. Its first line is the shebang:
#!/usr/bin/env bashmeans "run this file with bash". (Chapter 6 teaches scripting properly; here you only need a four-line loop.) - Linux only runs a file as a program if it has the executable permission (the "x bit").
chmod +x fileswitches it on (chmod= change mode,+x= add execute).ls -lshows it as anxin the first column, e.g.-rwxr-xr-x. Chapter 4 explains the whole permission string.
Foreground, not background
A program is in the foreground when it keeps running and holds on to the terminal until it is done, printing its output as it goes. A traditional daemon instead daemonises: it starts a copy of itself in the background and the original exits straight away.
Under systemd, your program should stay in the foreground and print its messages to stdout (the normal output stream). systemd is the one that runs it in the background, and it catches everything the program prints. If your script backgrounds itself (& at the end of a line), systemd sees the process it started exit immediately and thinks the service ended.
Type= decides what "started" means
Type= in [Service] tells systemd when to consider the service started:
simple (default) the program systemd runs IS the service. It counts as
started as soon as systemd has launched it. Use for any
foreground program: a web server, a Python app, your script.
exec like simple, but systemd waits until the program was actually
launched successfully. Slightly better error reporting.
forking the old daemon style: the program starts a background copy of
itself and the original exits. Needs PIDFile= (a file where the
daemon writes its PID) so systemd knows which process to watch.
oneshot runs, does one job, exits. systemd waits for it to finish.
Add RemainAfterExit=yes if it should still show as active after.
notify the program tells systemd "I am ready now" itself. Best of all -
"started" then means "actually ready" - but the program must be
written to do it.
The rules for ExecStart
ExecStart= is the command systemd runs. It is not typed into a shell, so:
- Use the absolute path. Always (the full path from
/, like/usr/bin/python3). systemd does not use your$PATH(the list of directories your shell searches for commands, 1.9). A bare name is only looked up in a fixed list (/usr/local/sbin, /usr/local/bin, /usr/sbin, /usr/bin), so anything in ~/bin is never found.~does not expand either - it produces a "bad-setting" load error. - No shell syntax: no pipes (
|), no&&, no redirection (>), no*. If you really need them, run a shell explicitly:/bin/bash -c '...'. - The file must be executable, and if it is a script it needs the shebang.
A minimal unit
[Unit]
Description=systemd service to run looped script
[Service]
User=learner
ExecStart=/home/learner/oncall-lab/labs/1a-linux/systemd/demo/script.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
User=learner runs it as your user instead of root (the all-powerful admin account) - a service should have only the power it needs.
The commands after writing it
sudo systemctl daemon-reload # make systemd re-read unit files from disk
sudo systemctl enable --now demo # start at every boot (enable) AND start now
systemctl status demo # did it work?
journalctl -u demo -f # watch its output live; Ctrl+C to stop
daemon-reloadtells systemd to read all unit files again. systemd reads them once and keeps them in memory; edits on disk are ignored until you reload.startvsenable:startruns it now.enablemakes it start at boot, by creating a symlink in/etc/systemd/system/multi-user.target.wants/(the list of units "wanted" by multi-user.target - that is whatWantedBy=described).--nowdoes both at once.journalctlreads the journal: systemd's central log, where it stores every line each service prints, with a timestamp and the unit's name.-u demo= only this unit;-f= follow (keep printing new lines as they arrive).
daemon-reload is not optional. Edit a unit without it and systemd keeps using the old definition. There is no error - only a one-line Warning: The unit file ... changed on disk. Run 'systemctl daemon-reload' above the output, which is easy to skim past. You will lose twenty minutes to this exactly once. (A brand-new unit file is picked up on first use; it is changes that need the reload. Run it every time anyway.)
Anything your service writes to stdout or stderr goes straight into the journal, tagged with the unit. That is why services today do not write their own log files.
What you can now do
- turn a foreground program into a service that survives logout and reboot
- explain start vs enable, and why daemon-reload matters
- follow a service's output with
journalctl -u NAME -f