Every Linux machine you've ever used starts the same way: the kernel boots, then hands control to exactly one process — PID 1 — whose job is to start everything else. For decades that process was a simple script runner called init. Today, on the overwhelming majority of Linux distributions, it's systemd — and "simple" is not a word anyone uses to describe it anymore. systemd doesn't just start your services; it manages logging, scheduling, mounting, networking, and device detection too. Depending on who you ask, that's either the best thing to happen to Linux administration in a decade, or a cautionary tale about scope creep. Both opinions are defensible, and you'll want to understand systemd either way — it's very likely running the machine in front of you right now.
Traditional Linux init (SysVinit) started services one at a time, in a fixed numeric order, using shell scripts — reliable, simple, and slow, since each service had to wait its turn even when nothing forced it to. In 2010, Lennart Poettering and Kay Sievers, both working at Red Hat, announced systemd as a replacement: an init system built around starting services in parallel wherever possible, tracking them precisely with Linux control groups (cgroups), and activating things on demand instead of unconditionally at boot.
The name follows a Unix convention — background processes ("daemons") traditionally end in d — so "system" + "d" = the daemon that manages the system. Fedora adopted it as the default in 2011; Red Hat Enterprise Linux 7, Debian, Ubuntu, SUSE, and Arch followed over the next several years. The transition wasn't quiet — Debian's 2014 decision to adopt systemd triggered enough internal disagreement that a group of developers forked the distribution into Devuan specifically to keep SysVinit alive as an option. That controversy is worth knowing about, not just as trivia: it's the direct result of systemd's central design tension, which the closing section returns to.
systemd organizes everything it manages into units — named, typed configuration files that describe one thing to start, stop, or track. The most common unit types:
| Unit type | Manages |
|---|---|
.service |
A long-running or one-shot program (a daemon, a script) |
.socket |
A network or Unix socket, often paired with a .service for on-demand activation |
.timer |
A scheduled trigger, systemd's answer to cron |
.mount / .automount |
A filesystem mount point |
.target |
A named grouping of other units — the modern replacement for SysVinit "runlevels" |
.device |
A piece of hardware detected by udev |
You interact with units almost entirely through one command: systemctl.
systemctl status nginx.service # is it running? recent log lines?
systemctl start nginx.service # start it now
systemctl stop nginx.service # stop it now
systemctl restart nginx.service # stop then start
systemctl enable nginx.service # start it automatically on future boots
systemctl enable --now nginx.service # enable AND start in one command
systemctl disable nginx.service # stop starting it automatically
enable and start are independent — this trips up nearly everyone at some point. enable wires the unit into the boot sequence for next time; start runs it right now. Doing one without the other is a common source of "but I just enabled it!" confusion.
Here's a real, minimal service unit — a small web app running as its own user:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/start.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
[Unit] — metadata and ordering. After=network.target says "don't start me until networking is up" — note that this controls order, not whether the dependency actually exists; for that you'd add Requires= or the gentler Wants= alongside it. (Also worth knowing: network.target only means the networking stack has been started, not that you have a working connection; services that genuinely need connectivity should use Wants=network-online.target and After=network-online.target.)[Service] — how to run it. Type=simple means "the command I gave you is the running process"; other values (forking, oneshot, notify) describe programs that background themselves or need to signal readiness explicitly. Restart=on-failure is a one-line reliability upgrade SysVinit never gave you for free.[Install] — what enable actually wires up. WantedBy=multi-user.target says "when the normal multi-user boot target starts, start me too."After creating or editing a unit file, you must tell systemd to notice:
sudo systemctl daemon-reload
Forgetting this is one of the most common systemd mistakes — you edit the file, restart the service, and nothing changes, because systemd is still running on its cached copy of the old configuration. (Recent versions print a warning that the unit file changed on disk; it's easy to scroll right past.)
Where SysVinit had numbered runlevels (0–6), systemd has named targets that are really just units that group other units:
| Target | Roughly equivalent to |
|---|---|
poweroff.target |
Runlevel 0 |
rescue.target |
Runlevel 1 (single-user) |
multi-user.target |
Runlevel 3 (normal, no GUI) |
graphical.target |
Runlevel 5 (normal, with GUI) |
reboot.target |
Runlevel 6 |
systemctl get-default # what does this machine boot into?
systemctl isolate rescue.target # switch targets right now (careful!)
A .timer unit schedules when to run something; it pairs with a same-named .service unit (backup.timer triggers backup.service, unless you point it elsewhere with Unit=) that defines what to run:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true is the feature plain cron never had (anacron exists to fill that gap): if the machine was off at 2am, the job runs as soon as it's back on, instead of silently skipping that day. Enable the timer (not the service) with systemctl enable --now backup.timer, and check what's scheduled with systemctl list-timers.
A .socket unit lets systemd own a port or Unix socket before the real service is running, and start that service only the instant a connection actually arrives — the same idea inetd pioneered decades earlier, generalized across the whole system. The payoff: services that are technically "available" all the time but consume zero resources until someone actually uses them.
systemd captures the stdout/stderr and metadata of everything it runs into a structured, indexed binary log via journald — no more hunting across a dozen text files in /var/log. (On some distributions the journal is kept only in memory and lost at reboot unless /var/log/journal exists or Storage=persistent is set in journald.conf, and many systems still run rsyslog alongside it.)
journalctl -u nginx.service # everything logged by this one unit
journalctl -u nginx.service -f # follow it live, like tail -f
journalctl --since "1 hour ago"
journalctl -p err # only error-priority messages and above
journalctl -b # everything since the current boot
When a service refuses to start, journalctl -xe (extra detail, jump to the end) is almost always your first move — it will usually tell you exactly which line of your unit file or which failed dependency is the actual problem.
daemon-reload after editing a unit file — systemd keeps running on the old, cached copy until you tell it to reload.enable with start — one is "on boot," the other is "right now." You often want both, via enable --now.Type= — a Type=simple service that actually forks and exits immediately will look "started" for a split second and then drop to inactive — and systemd may kill the forked child along with it — confusing everyone until the Type= is fixed to match reality (Type=forking for traditional daemons).systemctl edit myapp.service, which creates a small override in /etc/systemd/system/myapp.service.d/override.conf — your changes survive a package upgrade this way; direct edits to the original get silently clobbered.disable and mask — disable just removes it from the boot targets; mask links it to /dev/null, blocking it from being started even manually, by you or anything else, until it's unmasked. Masking a unit you meant to merely disable can cause very confusing "Unit is masked" failures later.| Command | What it tells you |
|---|---|
systemctl status <unit> |
Current state, recent log lines, process tree |
systemctl list-units --failed |
Everything currently broken |
systemd-analyze |
Total boot time |
systemd-analyze blame |
Which units took the longest to start, sorted |
systemd-analyze critical-chain |
The actual dependency chain that determined boot time |
systemd-cgtop |
Live resource usage, grouped by cgroup/unit |
hostnamectl / timedatectl / loginctl |
Hostname, clock/timezone, and login-session management, respectively |
systemd's defenders point out, correctly, that parallel startup, dependency-aware ordering, structured logging, cgroup-based resource limits, and a consistent configuration format across distributions are genuine, measurable improvements over a pile of ad-hoc shell scripts. Its critics point out, also correctly, that a single project now controls init, logging, login sessions, network configuration (systemd-networkd), DNS resolution (systemd-resolved), and more — a lot of surface area for something whose original job was "start processes in the right order." Both things are true at once, which is exactly why the debate hasn't settled and probably won't.
What isn't in dispute: if you administer Linux servers today, you are going to meet systemd, usually while debugging why a service won't start. Learn systemctl status, journalctl -xe, and daemon-reload — that's 90% of the day-to-day, and it will save you an hour of confusion the first time a service you just edited refuses to behave.
Tell us what you learned from this article, or what you liked about it. We read every comment.
Know someone who'd love it too?