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
systemctlpower moves:cat,edit,mask,daemon-reload, targets- Writing your own service, and the SELinux trap on Rocky
- Bonus:
sysctl, which is not the same assystemctl
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.
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:
| Era | Rocky / RHEL family | Ubuntu / Debian family |
|---|---|---|
| 1980s–2000s: SysV init | RHEL / CentOS up to 5 | Debian up to 7 |
| The in-between years | RHEL 6 used Upstart under the hood (commands still looked SysV) | Ubuntu 6.10–14.10 used Upstart, which Ubuntu invented |
| systemd | since 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:
| Runlevel | Meaning | systemd target today |
|---|---|---|
0 | Power off | poweroff.target |
1 | Single-user / rescue mode | rescue.target |
3 | Multi-user, text only (servers) | multi-user.target |
5 | Multi-user with a graphical desktop | graphical.target |
6 | Reboot | reboot.target |
The old commands still exist, but on a systemd machine they all just pass your request on to systemctl:
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.
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.
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:
.service: a program to run (sshd, cron, your web server).target: a group of units, the modern version of runlevels.timer: a schedule, like cron (systemctl list-timers).socket: start a service only when someone connects. Ubuntu 24.04 starts SSH this way, throughssh.socket.
Each unit is described by a small text file. There are two places they live, and one is more important:
| Folder | Who puts files there | Should 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 admin | Yes. Files here win over the package's version. |
systemctl cat sshd # (Ubuntu: ssh) show the unit file, with its pathA 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:
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
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?
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:
#!/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:
[Unit] Description=Heartbeat demo service [Service] ExecStart=/usr/local/bin/heartbeat.sh User=nobody Restart=on-failure [Install] WantedBy=multi-user.target
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
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
User=nobody: services should run as the least powerful user that works. If one gets hacked, the attacker only getsnobody's powers, not root's.- Full paths in
ExecStart=, just like cron. - Forget
chmod +xand the service fails withstatus=203/EXEC(“couldn't execute”).
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.
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?
- Needs to run all the time (a web server, a bot) → a service.
- Needs to run on a schedule (a backup at 2am) → cron (Linux Sysadmin, lesson 5), or a systemd timer.
- Needs to run once at boot → a service with
Type=oneshot.
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:
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?
✓ The old command still works. It just hands the job to systemctl.
2. You edited /etc/systemd/system/heartbeat.service, restarted it, and nothing changed. Why?
✓ Change a unit file → daemon-reload → restart.
3. What does systemctl enable crond actually do?
✓ enable = at boot. start = now. enable --now = both.
4. Old runlevel 3 is the same as which systemd target?
✓ 3 = multi-user, text only. That's how almost every server runs.
Next up: Linux Basics 2 · Everyday skills, starting with “Text editors: vim (and nano)”.