Services and systemd
Managing background services with systemd: units, systemctl, starting versus enabling, and reading service logs with journalctl.
What systemd is
When the kernel has finished starting, it runs exactly one program and gives it process ID (PID) 1. That program, the init system, starts everything else: it mounts the file systems, brings up the network, starts the daemons that provide services, and finally the login prompt or the desktop. When you shut down, it stops them all again in the right order.
On almost every current distribution — Debian, Ubuntu, RHEL (Red Hat
Enterprise Linux), Fedora, SUSE, Arch — that program is
systemd: the init system and service
manager that starts the system, starts services in parallel based on
their dependencies, supervises them (and restarts them if configured
to), and records their output in a central log, the journal. It
replaced the older SysV (System V) init scripts in
/etc/init.d, which started services one after another
from shell scripts.
$ ps -p 1 -o pid,comm
$ systemctl --version
PID COMMAND
1 systemd
systemd 255 (255.4-1ubuntu8.10)
+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT -GNUTLS +OPENSSL +ACL …
Old habits still work: service nginx restart and
/etc/init.d/nginx restart are passed on to systemd on
most systems. Learn systemctl anyway; it is what the
documentation, the exams and every error message refer to.
Units and unit types
systemd manages units: anything it can start, stop
or watch. Each unit is described by a
unit file, a small text file in the
INI (initialization file) style (sections in square brackets,
Key=value
lines) whose name ends in the unit type. The file says what to run,
what it depends on, and when it should start.
| Type | Describes | Example |
|---|---|---|
.service |
A program to run and supervise: a daemon or a one-off task | nginx.service, cron.service |
.socket |
A network or local socket systemd listens on; the matching service is started at the first connection |
ssh.socket (Ubuntu starts its Secure Shell
(SSH) server this way)
|
.timer |
A schedule that starts a service (the systemd alternative to cron; see Scheduling Tasks) | logrotate.timer |
.mount |
A file system mounted at a directory | home.mount for /home |
.target |
A group of units: a state the system can be in | multi-user.target |
You will also meet .path (start a service when a file
changes), .device and .slice units. In
commands, the .service suffix may be left out:
systemctl status nginx means
nginx.service.
Where unit files live
| Directory | Who writes there | Priority |
|---|---|---|
/usr/lib/systemd/system/ |
Packages (the "vendor" files). Overwritten on every update.
Older Debian releases use /lib/systemd/system/.
|
Lowest |
/run/systemd/system/ |
Generated at runtime; gone after a reboot | Middle |
/etc/systemd/system/ |
You, the administrator: your own units and overrides | Highest — wins over the others |
Controlling services with systemctl
systemctl is the command-line tool
for talking to systemd: starting, stopping, enabling and inspecting
units and changing the system state. Looking is allowed for every
user; changing needs root, so those commands run with
sudo.
Reading systemctl status
$ systemctl status nginx
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-09-30 08:14:02 UTC; 2h 3min ago
Docs: man:nginx(8)
Process: 1021 ExecStartPre=/usr/sbin/nginx -t -q -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Process: 1024 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=0/SUCCESS)
Main PID: 1026 (nginx)
Tasks: 3 (limit: 4556)
Memory: 4.2M (peak: 6.1M)
CPU: 38ms
CGroup: /system.slice/nginx.service
├─1026 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
├─1027 "nginx: worker process"
└─1028 "nginx: worker process"
Sep 30 08:14:02 web01 systemd[1]: Starting nginx.service - A high performance web server and a reverse proxy server...
Sep 30 08:14:02 web01 systemd[1]: Started nginx.service - A high performance web server and a reverse proxy server.
-
The dot in front: a filled
●(often green) is running, a white○is inactive, a red×has failed. -
Loaded: which unit file systemd read, and whether
the unit is
enabled(starts at boot) ordisabled. The preset is the distribution's default for new installs. -
Active: the state right now —
active (running),inactive (dead)(not started, not an error), orfailed— and since when. - Main PID and CGroup: the main process and every process that belongs to the service. systemd tracks them with a control group (cgroup), so a stop really stops all of them.
- The last log lines of the unit, from the journal. For a failed service, the reason is usually here.
systemctl status opens in a pager when the output is
long; press q to leave, or add --no-pager. A
failed service looks like this (captured before
Restart=on-failure was added to the
unit file; with it, systemd keeps retrying
and the status shows activating (auto-restart)
between attempts):
$ systemctl status inventory-api
× inventory-api.service - Inventory API (internal web application)
Loaded: loaded (/etc/systemd/system/inventory-api.service; enabled; preset: enabled)
Active: failed (Result: exit-code) since Wed 2026-09-30 10:02:17 UTC; 4min ago
Duration: 12ms
Process: 2311 ExecStart=/opt/inventory/venv/bin/python -m inventory.server --port 8080 (code=exited, status=203/EXEC)
Main PID: 2311 (code=exited, status=203/EXEC)
CPU: 4ms
Sep 30 10:02:17 web01 systemd[1]: Started inventory-api.service - Inventory API (internal web application).
Sep 30 10:02:17 web01 (python)[2311]: inventory-api.service: Failed to locate executable /opt/inventory/venv/bin/python: No such file or directory
Sep 30 10:02:17 web01 (python)[2311]: inventory-api.service: Failed at step EXEC spawning /opt/inventory/venv/bin/python: No such file or directory
Sep 30 10:02:17 web01 systemd[1]: inventory-api.service: Main process exited, code=exited, status=203/EXEC
Sep 30 10:02:17 web01 systemd[1]: inventory-api.service: Failed with result 'exit-code'.
status=203/EXEC is systemd's own code for "the program
in ExecStart could not be executed" — here because the
path is wrong. Statuses from 200 up, shown with a name such as
203/EXEC, 200/CHDIR or
217/USER, are systemd's own: the program never started.
Other exit statuses come from the program itself, and its own error
message is then in the log lines.
start, stop, restart and reload
| Command | Effect |
|---|---|
sudo systemctl start nginx |
Start it now (nothing happens if it already runs) |
sudo systemctl stop nginx |
Stop it now |
sudo systemctl restart nginx |
Stop and start: all connections are dropped for a moment |
sudo systemctl reload nginx |
Ask the running service to re-read its configuration without
stopping — only if the unit defines how
(ExecReload)
|
sudo systemctl reload-or-restart nginx |
Reload if the service supports it, otherwise restart |
These commands print nothing when they succeed. Check the result
with systemctl status or the short checks
is-active, is-enabled and
is-failed, which print one word and set the exit
status, so scripts can use them:
$ sudo nginx -t && sudo systemctl reload nginx
$ systemctl is-active nginx
$ systemctl is-enabled nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful active enabled
Finding failed units
$ systemctl list-units --failed
$ systemctl list-units --type=service --state=running
UNIT LOAD ACTIVE SUB DESCRIPTION
● inventory-api.service loaded failed failed Inventory API (internal web application)
Legend: LOAD → Reflects whether the unit definition was properly loaded.
ACTIVE → The high-level unit activation state, i.e. generalization of SUB.
SUB → The low-level unit activation state, values depend on unit type.
1 loaded units listed.
…
list-units --failed is a good first command on any
server you log in to after a problem report. After fixing the cause,
sudo systemctl reset-failed inventory-api clears the
failed state. systemctl list-unit-files --type=service
lists every installed service with its enabled or disabled state,
including those that are not running.
Starting versus enabling
This is the most important distinction on this page.
start acts now and is forgotten at the next
reboot. enable acts at boot: an
enabled service is a unit
that systemd starts automatically when the system boots, because
enable linked it into a target's list of wanted units.
Enabling does not start it now, and starting does not enable it.
$ sudo systemctl enable nginx
$ sudo systemctl enable --now nginx
$ sudo systemctl disable --now nginx
Created symlink /etc/systemd/system/multi-user.target.wants/nginx.service → /usr/lib/systemd/system/nginx.service. Removed "/etc/systemd/system/multi-user.target.wants/nginx.service".
The output shows what enabling really is: a symbolic link in
/etc/systemd/system/multi-user.target.wants/, created
from the [Install] section of the unit file (see
below). The second command printed nothing
because the link already existed; --now also started
the service. disable --now removes the link and stops
it. On Debian and Ubuntu, nginx is already enabled right after
installation, and each enable or disable
there also prints two lines about
systemd-sysv-install (the package still ships an old
init script, which is kept in sync).
| Runs now? | Starts at boot? | |
|---|---|---|
start |
Yes | No (unless it was already enabled) |
enable |
No (unless it was already running) | Yes |
enable --now |
Yes | Yes |
disable --now |
No | No |
mask |
Cannot be started at all, not even by hand or as a dependency | No |
mask links the unit to /dev/null; use it
for a service that must never run (for example one of two
conflicting firewall services), and unmask to undo it.
Targets
A target is a unit that groups other units and represents a state of the system, such as "fully booted with networking, no desktop". Booting means reaching the default target, and everything that target wants — directly or through other targets — is started.
graphical.target pulls in
multi-user.target, which pulls in the services
enabled for it. A timer starts its service on a schedule instead
of at boot.
| Target | State | Old SysV runlevel |
|---|---|---|
multi-user.target |
Fully running, networking, many users, text logins — the normal default on servers | 3 |
graphical.target |
multi-user.target plus a graphical login — the
default on desktops
|
5 |
rescue.target |
Basic system and local file systems, a root shell, no network services | 1 |
emergency.target |
Almost nothing: a root shell with the root file system mounted read-only | — |
poweroff.target, reboot.target
|
Shutting down or restarting | 0, 6 |
$ systemctl get-default
$ sudo systemctl set-default multi-user.target
graphical.target Removed "/etc/systemd/system/default.target". Created symlink /etc/systemd/system/default.target → /usr/lib/systemd/system/multi-user.target.
set-default changes the target for the next boots;
sudo systemctl isolate multi-user.target switches right
away (and stops everything the new target does not want, such as the
desktop). Rescue and emergency mode are recovery tools, usually
chosen at boot time; when and how to use them is covered in
Troubleshooting Linux.
A unit file, line by line
Suppose a small internal web application should run permanently on a
server, restart if it crashes, and start at boot. Its unit file,
written by you, belongs in /etc/systemd/system/:
[Unit]
Description=Inventory API (internal web application)
Documentation=https://wiki.example.com/inventory-api
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=inventory
Group=inventory
WorkingDirectory=/opt/inventory
ExecStart=/opt/inventory/venv/bin/python -m inventory.server --port 8080
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
| Line | Meaning |
|---|---|
[Unit] |
General information and ordering, valid for any unit type |
Description= |
The text shown in status and in the logs |
Documentation= |
Where a colleague can read more (a web address or
man: page)
|
Wants= / After= |
Wants pulls in another unit;
After only sets the order. Together: "start
once the network is really up". They are independent, so you
usually need both.
|
[Service] |
How to run the program (only in .service units)
|
Type=simple |
The program stays in the foreground and is itself the
service (the default). forking is for old-style
daemons that put themselves in the background;
oneshot for a task that runs and ends.
|
User= / Group= |
Run as this unprivileged account instead of root — least privilege: a bug in the application cannot take over the whole system. |
WorkingDirectory= |
The directory the program starts in |
ExecStart= |
The command to run. It is not run by a shell: use absolute
paths, and no pipes, redirection or &.
|
Restart=on-failure |
Restart the program when it crashes or exits with an error, but not when you stop it on purpose |
RestartSec=5 |
Wait 5 seconds before restarting |
[Install] |
What enable should do (ignored at runtime)
|
WantedBy=multi-user.target |
enable creates the link in
multi-user.target.wants/, so the service starts
at every normal boot
|
Then tell systemd about the new file, and enable and start it in one go:
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now inventory-api
$ systemctl status inventory-api
daemon-reload makes systemd re-read all unit files. It
is needed every time you create or change a unit file by hand;
without it systemd keeps using the old version (and
status warns:
Warning: The unit file, source configuration file or drop-ins of
inventory-api.service changed on disk. Run 'systemctl
daemon-reload' to reload units.
). Note the difference: daemon-reload reloads
systemd's unit files, while reload nginx makes
nginx re-read its own configuration.
Changing a service: overrides with systemctl edit
Vendor unit files in /usr/lib/systemd/system/ belong to
the package: the next update overwrites your changes without
warning. To change a setting, add a drop-in: a
small file with only the lines you want to change, which systemd
reads after the original.
$ sudo systemctl edit nginx
This opens an editor on
/etc/systemd/system/nginx.service.d/override.conf
(created if needed), shows the original file as comments for
reference, and runs daemon-reload for you when you
save. To make nginx restart automatically after a crash, the
override only needs:
[Service]
Restart=on-failure
RestartSec=10
$ sudo systemctl restart nginx
$ systemctl cat nginx
# /usr/lib/systemd/system/nginx.service [Unit] Description=A high performance web server and a reverse proxy server … # /etc/systemd/system/nginx.service.d/override.conf [Service] Restart=on-failure RestartSec=10
systemctl cat shows the complete, effective
configuration with the file each part comes from — the first thing
to check when a service does not behave as its unit file suggests.
sudo systemctl revert nginx removes all your drop-ins
and returns to the vendor version.
systemctl edit --full copies the whole unit to
/etc/systemd/system/ instead; that works, but then you
no longer receive the vendor's fixes to it.
Reading a service's log
Everything a service writes to its standard output and standard
error ends up in the systemd journal, together with systemd's own
messages about that unit. journalctl reads it; these
options cover most service problems:
$ journalctl -u nginx --since "1 hour ago"
$ journalctl -u inventory-api -b
$ journalctl -u inventory-api -f
$ journalctl -u inventory-api -n 50 --no-pager
| Option | Meaning |
|---|---|
-u name |
Only this unit (repeat -u for several) |
-b |
Only since the current boot (-b -1: the
previous boot)
|
-f |
Follow: keep printing new lines until Ctrl+C |
--since / --until |
A time window: "1 hour ago",
today, "2026-09-30 10:00"
|
-n 50, -e |
The last 50 lines; jump to the end in the pager |
A useful habit while testing: keep
journalctl -u name -f running in a second terminal
while you start or restart the service in the first. Reading logs in
depth — priorities, the kernel log, classic log files and log
rotation — is covered in
Logs and Monitoring.