Units and systemctl
systemd manages everything through units — typed objects with declared dependencies. systemctl is the tool to inspect, start, stop, enable, and disable them. Enabled means start at boot; active means running right now.
You type sudo systemctl restart nginx after a config change and the service comes back up in under a second — no shell script, no PID file hunting, no manual /etc/init.d/nginx stop && sleep 2 && /etc/init.d/nginx start. But when the server boots and nginx is mysteriously not running, you type systemctl status nginx and immediately see it exited with code 1 three seconds into boot because /var/log/nginx did not yet exist. You knew in ten seconds what would have taken ten minutes of log archaeology before systemd. That debugging speed is what systemd’s unit model buys you.
After this lesson you can name the six major unit types and what each manages, use systemctl to start, stop, enable, disable, and inspect units, explain the difference between enabled and active, and read systemctl status output to identify why a service failed.
Everything systemd manages is a unit — a typed object with a name ending in its type extension. The six types you will meet most often:
| Extension | What it manages |
|---|---|
.service | A daemon or one-shot process |
.socket | A network or IPC socket (for socket activation) |
.timer | A scheduled trigger (replaces cron) |
.target | A named synchronisation point or group |
.mount | A filesystem mount (auto-generated from fstab) |
.path | A filesystem path watch |
Unit names follow the pattern name.type: sshd.service, multi-user.target, apt-daily.timer. The type tells systemd which unit file keys are valid and which activation model to use.
# List all loaded units, with their state
systemctl list-units
# Filter to only service units
systemctl list-units --type=service
# List ALL known units, even those not loaded
systemctl list-unit-filessystemctl status is the first command you run when something is wrong — and it tells you a lot. The output is dense but structured:
systemctl status nginx.serviceA typical healthy output:
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2024-01-15 09:22:34 UTC; 3h 12min ago
Docs: man:nginx(8)
Main PID: 1234 (nginx)
Tasks: 2 (limit: 4672)
Memory: 5.8M
CPU: 421ms
CGroup: /system.slice/nginx.service
├─1234 "nginx: master process /usr/sbin/nginx -g daemon off;"
└─1235 "nginx: worker process"
Jan 15 09:22:34 hostname systemd[1]: Starting A high performance web server...
Jan 15 09:22:34 hostname systemd[1]: Started A high performance web server.Key fields: Loaded shows whether systemd has parsed the unit file and whether it is enabled. Active is the runtime state — active (running), active (exited), inactive (dead), or failed. The CGroup block shows the exact process tree. The log tail shows recent journal entries for this unit only.
Start, stop, and restart control the runtime state. These changes survive only until the next reboot — they do not affect whether the unit starts automatically:
# Start a stopped unit
sudo systemctl start nginx.service
# Stop a running unit (sends SIGTERM, then SIGKILL after timeout)
sudo systemctl stop nginx.service
# Restart: stop then start (replaces the process)
sudo systemctl restart nginx.service
# Reload: send SIGHUP to reload config without stopping (if the service supports it)
sudo systemctl reload nginx.service
# Reload-or-restart: prefer reload, fall back to restart
sudo systemctl reload-or-restart nginx.serviceYou can omit the .service suffix when there is no ambiguity — systemctl restart nginx works the same as systemctl restart nginx.service.
Enable and disable control whether a unit starts at boot — this is separate from whether it is running now. Enable installs symlinks into the target directory; disable removes them:
# Enable: create symlink so it starts at boot
sudo systemctl enable nginx.service
# Enable AND start right now (common for new services)
sudo systemctl enable --now nginx.service
# Disable: remove the symlink (does not stop the running service)
sudo systemctl disable nginx.service
# Disable AND stop right now
sudo systemctl disable --now nginx.serviceAfter enable, the Loaded: line in systemctl status changes from disabled to enabled. The unit is now wired into multi-user.target (or whatever WantedBy= says in the [Install] section). After disable, the symlink is removed — the unit will not start at next boot, but if it is currently running it keeps running until stopped or rebooted.
Active vs enabled is the most common source of confusion for new operators. They are independent axes:
| Enabled (starts at boot) | Disabled (does not start at boot) | |
|---|---|---|
| Active (running now) | Normal production service | Started manually this session |
| Inactive (not running) | Will start at next reboot | Off completely |
A service can be running but disabled (you started it manually, it won’t survive reboot), or enabled but not running (it failed to start). Use is-active and is-enabled for scripting:
# Exit code 0 if active, non-zero if not
systemctl is-active nginx.service
# Exit code 0 if enabled, non-zero if not
systemctl is-enabled nginx.service
# Combine to check both
if systemctl is-active --quiet nginx && systemctl is-enabled --quiet nginx; then
echo "nginx is healthy and persistent"
fiDiagnosing a service that was running yesterday but isn’t today.
After an unplanned reboot, a developer reports that their team’s custom API service is not responding. Here is the operator workflow:
# Step 1: check the current state
systemctl status myapi.service
# Output: Active: inactive (dead)
# Loaded: loaded (/etc/systemd/system/myapi.service; disabled; ...)
# Step 2: the 'disabled' tells the whole story
# It was started manually but never enabled — did not survive the reboot.
# Step 3: fix for now AND for future reboots
sudo systemctl enable --now myapi.service
# Step 4: verify
systemctl status myapi.service
# Active: active (running)
# Loaded: loaded (...; enabled; ...)The mistake was that whoever deployed the service ran systemctl start but not systemctl enable. The service worked fine until the first reboot.
▸Why this works
macOS uses launchctl and launchd plist files instead of systemd unit files. The concept maps: a .plist in /Library/LaunchDaemons/ is analogous to a .service file with WantedBy=multi-user.target. launchctl load is like systemctl enable --now. There is no enabled vs active distinction in launchd — loading a plist both registers and starts it.
▸Common mistake
A common mistake: editing /lib/systemd/system/nginx.service directly instead of creating an override in /etc/systemd/system/. Files under /lib/ are owned by the package manager and will be silently overwritten on the next apt upgrade. Always put local customisations in /etc/systemd/system/ — either a full replacement file or a drop-in directory (nginx.service.d/override.conf).
You run `systemctl enable myapp.service` and get no errors. An hour later a colleague checks and finds myapp is not running. Which of the following best explains this?
systemd models the entire system as units — typed objects (.service, .socket, .timer, .target, .mount, .path) with declared dependencies. systemctl is the CLI to manage them. The most important conceptual split: enabled (boot-time symlink installed) is independent of active (currently running). systemctl start makes something run now; systemctl enable makes it survive reboots. systemctl status shows both axes plus the journal tail — it is the first command to run when something looks wrong. Always put local unit customisations in /etc/systemd/system/, never in /lib/systemd/system/.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.
Apply this
Put this lesson to work on a real build.