Linux Basics · Lesson 15 · 25 min

Services deep dive: systemd, init.d & service

In lesson 14 you started a web server with systemctl. Now let's look under the hood. What actually starts all those services when a Linux machine boots? Why do old tutorials say service httpd restart or /etc/init.d/apache2 start? And how do you turn your own script into a proper service?

You will learn

  • What PID 1 is, and the story from SysV init to systemd
  • The old tools (/etc/init.d, service, chkconfig, update-rc.d, runlevels) and what they do today
  • systemd unit files: where they live, how to read them, and what “enabled” really means
  • systemctl power moves: cat, edit, mask, daemon-reload, targets
  • Writing your own service, and the SELinux trap on Rocky
  • Bonus: sysctl, which is not the same as systemctl

PID 1: the first program

When Linux boots, the firmware starts the bootloader (GRUB), which loads the kernel. The kernel then starts exactly one program, with process ID 1. That program's job is to start everything else: networking, logging, SSH, cron, your web server. If PID 1 dies, the whole system goes down.

Same on both
ps -p 1 -o pid,comm     # who is PID 1?
ls -l /usr/sbin/init    # "init" is really a link to systemd
    PID COMMAND
      1 systemd

Today both families use systemd as PID 1. It wasn't always that way:

EraRocky / RHEL familyUbuntu / Debian family
1980s–2000s: SysV initRHEL / CentOS up to 5Debian up to 7
The in-between yearsRHEL 6 used Upstart under the hood (commands still looked SysV)Ubuntu 6.10–14.10 used Upstart, which Ubuntu invented
systemdsince RHEL / CentOS 7 (2014)since Debian 8 and Ubuntu 15.04 (2015)

You'll still meet the old ways constantly: in tutorials, on ancient servers, and in commands that were kept around so old scripts keep working. So you need to know both.

The old way: SysV init

Under SysV init, every service was a shell script in /etc/init.d/ that understood start, stop, restart and status. The system ran them in order at boot, based on the runlevel:

RunlevelMeaningsystemd target today
0Power offpoweroff.target
1Single-user / rescue moderescue.target
3Multi-user, text only (servers)multi-user.target
5Multi-user with a graphical desktopgraphical.target
6Rebootreboot.target

The old commands still exist, but on a systemd machine they all just pass your request on to systemctl:

Rocky / RHEL
service crond restart
chkconfig crond on
ls /etc/init.d
runlevel
Redirecting to /bin/systemctl restart crond.service
Note: Forwarding request to 'systemctl enable crond.service'.
functions  README
N 3

/etc/init.d (a link to /etc/rc.d/init.d) is basically empty. Its README literally starts with “You are looking for the traditional init scripts… and they are gone?” chkconfig was Red Hat's tool for choosing which services start at boot.

Ubuntu / Debian
service cron restart
sudo /etc/init.d/cron restart
sudo update-rc.d cron enable
runlevel
Restarting cron (via systemctl): cron.service.

N 3

Ubuntu still ships compatibility scripts in /etc/init.d for many packages. Run one and it tells you it went “via systemctl.” update-rc.d was Debian's boot-time tool.

Rule of thumb

If a tutorial says service X restart or /etc/init.d/X restart, it will usually still work. But the modern, same-on-both way is sudo systemctl restart X. Use that in anything you write.

systemd: everything is a unit

systemd manages units. The most common kinds:

Each unit is described by a small text file. There are two places they live, and one is more important:

FolderWho puts files thereShould you edit it?
/usr/lib/systemd/system/Packages (dnf / apt)No. Updates will overwrite your changes. (Older Ubuntu shows this as /lib/systemd/system/, which is the same folder.)
/etc/systemd/system/You, the adminYes. Files here win over the package's version.
Same on both
systemctl cat sshd       # (Ubuntu: ssh) show the unit file, with its path

A unit file has three sections:

[Unit]                              ← what it is, and what it starts after
Description=OpenSSH server daemon
After=network.target

[Service]                           ← how to run it
ExecStart=/usr/sbin/sshd -D $OPTIONS   (the command — always a full path)
Restart=on-failure                     (if it crashes, start it again)

[Install]                           ← what "enable" should do
WantedBy=multi-user.target             (start it as part of normal boot)

What “enable” really does

systemctl enable doesn't start anything. It creates a symlink (a shortcut) inside the target named in WantedBy=. At boot, systemd starts everything linked in multi-user.target.wants:

Same on both
ls -l /etc/systemd/system/multi-user.target.wants/
lrwxrwxrwx 1 root root 37 crond.service -> /usr/lib/systemd/system/crond.service
lrwxrwxrwx 1 root root 36 sshd.service -> /usr/lib/systemd/system/sshd.service

disable removes the link. And mask goes further: it links the unit to /dev/null, so nothing can start it until you unmask it. That's handy when you want to be sure something stays off.

systemctl power moves

Same on both
systemctl list-units --type=service   # what's running
systemctl list-unit-files            # everything installed: enabled / disabled / masked / static
systemctl --failed                   # what crashed
systemctl cat NAME                   # read its unit file(s)
sudo systemctl edit NAME             # add an override ("drop-in") safely
sudo systemctl daemon-reload         # re-read unit files after YOU change them
sudo systemctl mask NAME             # make it impossible to start
systemctl get-default                # which target boots by default (runlevel)
systemd-analyze blame                # what slowed down booting?
Forgot daemon-reload?

If you change a unit file and restart without daemon-reload, systemd keeps using the old version and warns you: “The unit file … changed on disk. Run 'systemctl daemon-reload'”. systemctl edit reloads automatically. Editing the file by hand doesn't.

Drop-ins are the clean way to change a package's service. sudo systemctl edit cron creates /etc/systemd/system/cron.service.d/override.conf. Only the lines you add there change, and package updates can't undo them. For example, to make a service restart even after a clean stop:

[Service]
Restart=always

Build your own service

Let's turn a script into a real service that starts at boot, restarts if it crashes, and logs to the journal. First the script. It loops forever and prints a line every minute:

/usr/local/bin/heartbeat.sh
#!/bin/bash
while true; do
  echo "still alive at $(date +%H:%M)"
  sleep 60
done

Then the unit file. Everything a service prints goes straight into the journal:

/etc/systemd/system/heartbeat.service
[Unit]
Description=Heartbeat demo service

[Service]
ExecStart=/usr/local/bin/heartbeat.sh
User=nobody
Restart=on-failure

[Install]
WantedBy=multi-user.target
Rocky / RHEL
sudo vi /usr/local/bin/heartbeat.sh
sudo chmod +x /usr/local/bin/heartbeat.sh
sudo vi /etc/systemd/system/heartbeat.service
sudo systemctl daemon-reload
sudo systemctl enable --now heartbeat
journalctl -u heartbeat -f
Ubuntu / Debian
sudo nano /usr/local/bin/heartbeat.sh
sudo chmod +x /usr/local/bin/heartbeat.sh
sudo nano /etc/systemd/system/heartbeat.service
sudo systemctl daemon-reload
sudo systemctl enable --now heartbeat
journalctl -u heartbeat -f
The Rocky SELinux trap

On Rocky, point ExecStart= at a script in your home folder (/home/student/heartbeat.sh) and it fails with 203/EXEC: Permission denied, even though the permissions look perfect. SELinux doesn't let system services run programs stored in home folders. That's why programs belong in /usr/local/bin. Ubuntu's AppArmor doesn't block this case, so the same mistake “works” there, which is how people get surprised when they move to Rocky.

No editor handy?

You can write both files with one command each using printf (where \n means a new line) and sudo tee:

printf '#!/bin/bash\nwhile true; do\n  echo "still alive at $(date +%%H:%%M)"\n  sleep 60\ndone\n' | sudo tee /usr/local/bin/heartbeat.sh
printf '[Unit]\nDescription=Heartbeat demo service\n\n[Service]\nExecStart=/usr/local/bin/heartbeat.sh\nUser=nobody\nRestart=on-failure\n\n[Install]\nWantedBy=multi-user.target\n' | sudo tee /etc/systemd/system/heartbeat.service

Service, cron job, or timer?

Don't confuse it with sysctl!

systemctl controls services. sysctl (one letter away) changes kernel settings: networking, memory, security. The settings live as tiny files under /proc/sys:

Same on both
sysctl net.ipv4.ip_forward                    # read (0 = off)
sudo sysctl -w net.ipv4.ip_forward=1          # change now (gone after reboot)
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-router.conf
sudo sysctl --system                          # load all config files (permanent)

Both families read /etc/sysctl.d/*.conf at boot, so that's where your changes belong. Ubuntu's /etc/sysctl.conf is full of commented-out examples. Rocky's just explains the folders, and its package defaults live in /usr/lib/sysctl.d/.

Try it

Quick check

1. On Rocky you type service sshd restart. What happens?

2. You edited /etc/systemd/system/heartbeat.service, restarted it, and nothing changed. Why?

3. What does systemctl enable crond actually do?

4. Old runlevel 3 is the same as which systemd target?

Finished the missions and the quiz? Mark it done to track your progress.

Next up: Linux Basics 2 · Everyday skills, starting with “Text editors: vim (and nano)”.