systemctl: list, start and diagnose Linux services
List services with systemctl, start, stop and reload them, write a unit and diagnose a failure. list-units vs list-unit-files and the Type=simple trap.
The issue opens pre-filled with this page title and URL.
systemctl is the central command to manage services on Linux. It lists,
starts, stops, reloads and diagnoses everything systemd drives on a modern
server. This page covers the life cycle of a service, the two ways to
list services (the one most readers miss), how to write a unit and the
basic diagnosis of a service that refuses to run. Audience: a beginner
discovering systemctl, and an administrator tired of being fooled by an
over-optimistic start.
All the outputs shown here come from a Debian 13 server with systemd 257
and nginx installed. The commands are identical on Ubuntu, Debian and RHEL;
only a few enable outputs differ, and this is pointed out.
What you will learn
List services with list-units and list-unit-files, and understand why the two lists differ
Drive the life cycle: start, stop, restart, reload, status
Read a status line by line and query a service with is-active, is-enabled, is-failed
Write a custom service unit without falling into the Type=simple trap
Diagnose a failure with journalctl and the 203/EXEC code
The five everyday commands
Most of the daily work on services fits in five verbs. Each one acts
immediately, on the system as it runs right now, and none survives a
reboot of the machine: enabling at boot is a separate notion. All these
commands change the state of the system, so they need root privileges
through sudo, except status, which only reads.
Terminal window
sudosystemctlstartnginx# start now
sudosystemctlstopnginx# stop now
sudosystemctlrestartnginx# stop then start again
sudosystemctlreloadnginx# reread the configuration without cutting
systemctlstatusnginx# where do we stand?
The good habit is to chain the action and its check on the same command
line. The shell only runs the second part if the first one succeeded, and
is-active answers in one word:
Terminal window
systemctlstartnginx && systemctlis-activenginx
active
The .service suffix is optional: without a suffix, systemctl appends
.service by default, so systemctl start nginx and
systemctl start nginx.service are strictly equivalent. The suffix becomes
necessary to target another unit with the same name, a timer or a
socket: systemctl status logrotate shows the service, and only
systemctl status logrotate.timer shows the schedule.
Reload without cutting traffic
reload asks the service to reread its configuration while it keeps
serving the requests in progress. restart kills the process then starts a
new one: the open connections drop. On a production web server, the
difference shows in the client logs.
Terminal window
systemctlreloadnginx && systemctlis-activenginx
active
Read a status line by line
systemctl status is the identity card of a service: where its unit file
comes from, whether it runs, since when, with which PID, and the last lines
of its journal. It is the first reflex in front of a suspicious service.
Terminal window
systemctlstatusnginx
● nginx.service - A high performance web server and a reverse proxy server
├─81 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
├─82 "nginx: worker process"
├─83 "nginx: worker process"
(... one worker process per core, 17 tasks in total ...)
Jul 12 16:54:11 srv01 systemd[1]: Starting nginx.service - A high performance web server and a reverse proxy server...
Jul 12 16:54:11 srv01 systemd[1]: Started nginx.service - A high performance web server and a reverse proxy server.
Each line answers a precise question, and the first three are usually
enough to settle a diagnosis:
Line
What it tells you
Loaded
Path of the unit file in use, and whether the service is enabled at boot (enabled / disabled / masked)
Active
Current state and duration: active (running), inactive (dead), failed
Process
Commands already finished and their exit code (status=0/SUCCESS or an error)
Main PID
PID of the main process; if it is missing, nothing runs
CGroup
The real tree of processes attached to the service
Swipe to see more
The Loaded line is the one people forget to read. It gives the exact
path of the loaded unit file, which saves ten minutes of editing a file
that systemd does not use.
One-word answers
In a script, a full status is unusable: you need a short answer and a
reliable exit code. Three commands provide it, and they work just as well
by hand as in an Ansible automation or a deployment script.
Terminal window
systemctlis-activenginx# active | inactive | failed | activating
This is the most asked question about systemctl, and the answer is a trap:
there are two lists of services, and they do not contain the same things.
The command found everywhere (systemctl list-units) is precisely the one that
hides part of the services of the machine.
The distinction fits in one sentence: list-units lists the units loaded in
memory by systemd; list-unit-files lists the unit files installed on
disk. A service installed by a package but never started does exist on disk,
but systemd has not loaded it, so it does not appear in list-units. On the
demonstration server, the gap is striking: 14 loaded services against 119
installed unit files.
Services loaded in memory
systemctl list-units --type=service answers the question "what is systemd
handling right now?". It is the runtime view, the one you want when you
inspect a live server.
Terminal window
systemctllist-units--type=service
UNIT LOAD ACTIVE SUB DESCRIPTION
ldconfig.service loaded active exited Rebuild Dynamic Linker Cache
nginx.service loaded active running A high performance web server and a reverse proxy server
systemd-journal-catalog-update.service loaded active exited Rebuild Journal Catalog
systemd-journal-flush.service loaded active exited Flush Journal to Persistent Storage
systemd-journald.service loaded active running Journal Service
systemd-remount-fs.service loaded active exited Remount Root and Kernel File Systems
systemd-sysctl.service loaded active exited Apply Kernel Variables
systemd-sysusers.service loaded active exited Create System Users
systemd-tmpfiles-setup-dev-early.service loaded active exited Create Static Device Nodes in /dev gracefully
systemd-tmpfiles-setup-dev.service loaded active exited Create Static Device Nodes in /dev
systemd-tmpfiles-setup.service loaded active exited Create System Files and Directories
systemd-update-done.service loaded active exited Update is Completed
systemd-user-sessions.service loaded active exited Permit User Sessions
14 loaded units listed. Pass --all to see loaded but inactive units, too.
To show all installed unit files use 'systemctl list-unit-files'.
Three columns carry the useful information. LOAD says whether the unit
file was read correctly, ACTIVE gives the general state and SUB the
detail: a service in active (exited) finished its task successfully and left
no process behind, which is normal for start-up tasks. The black dot at
the start of a line flags the units that need your attention, here
systemd-modules-load.service in failed.
The last two lines of the output are a user manual that systemd gives you
for free: --all to also see the units that are loaded but inactive,
list-unit-files to see everything installed.
Services installed on disk
systemctl list-unit-files --type=service answers the question "what is
installed on this machine, and will it start at boot?". It is the disk
view, the one you want for an audit or to find a service you never
started.
The full output counts 119 lines; the excerpt above shows one example of
each state met. The STATE column is the heart of the matter: it says what
systemd will do with this service at the next boot. The PRESET column
shows what the distribution recommended by default, which lets you spot at a
glance the services whose state was changed by hand.
STATE
What it means
enabled
Will start at boot: a symbolic link exists in a .wants directory
disabled
Will not start at boot, but can still be started by hand
static
No [Install] section: cannot be enabled, it is pulled in by a dependency
masked
Linked to /dev/null: impossible to start, even manually
alias
Another name for an existing service (on Debian, kmod.service is a link to systemd-modules-load.service)
enabled-runtime
Enabled until the next reboot only, the enabling lives in /run and not in /etc
indirect
Can be enabled, but the real enabling goes through another service of the same group
generated
Built on the fly by a generator, typically from an old SysV script
Swipe to see more
The static state is the one that surprises the most. A
systemctl enable systemd-tmpfiles-clean.service does nothing, not because
of a mistake on your side, but because this service is not meant to be
enabled: other units trigger it. The command confirms it in one word.
Terminal window
systemctlis-enabledsystemd-tmpfiles-clean.service
static
The listings that really help
Listing the 119 units of a server has no operational value. What you want
is to filter: which services run, which ones fell over, which ones will
start at the next boot. The --state= filter applies to both commands, with
different values since they do not talk about the same thing.
The services that really run, with a live process to prove it:
Terminal window
systemctllist-units--type=service--state=running
UNIT LOAD ACTIVE SUB DESCRIPTION
nginx.service loaded active running A high performance web server and a reverse proxy server
systemd-journald.service loaded active running Journal Service
2 loaded units listed.
The services that will start at the next boot, the command to know for a
server audit:
Two options complete the kit. --all adds the units that are loaded but
inactive, which takes the list from 14 to 65 entries on the demonstration
machine: this is where the stopped services reappear. --no-legend
removes the header and the legend, which makes the output usable in a
script.
Terminal window
systemctllist-units--type=service--no-legend|wc-l
14
The table that prevents the mistake
Remember the question each command really asks. It is the only way to stop
hesitating in front of an unknown server.
A service goes through several states during its life, and
systemctl status shows them on the Active line. Knowing them avoids
false diagnoses: a service in active (exited) is not broken, it simply
finished its work.
State
Meaning
active (running)
The service runs, a process is alive
active (exited)
The task finished successfully, no process remains (Type=oneshot)
inactive (dead)
The service is stopped, without error
failed
The service failed, the journal holds the reason
activating
Start-up in progress
deactivating
Shutdown in progress
masked
The service is blocked, a start will refuse to do anything
Swipe to see more
enabled is not active
This is the starting confusion of every beginner, and it produces servers that
run perfectly until the first reboot. The two notions are independent:
one talks about the present, the other about the next boot.
Concept
Question asked
Command
active
Is the service running right now?
systemctl is-active nginx
enabled
Will it start on its own at the next boot?
systemctl is-enabled nginx
Swipe to see more
A service can be enabled and inactive: it is stopped now, it will come
back at reboot. It can be active and disabled: it runs, but it will not
survive the restart. This last case is the classic scenario of the service
installed on a Friday evening and gone on Monday morning.
The diagram separates the two axes: on the left the state right now,
changed by start and stop; on the right the state at the next boot,
changed by enable, disable and mask. No arrow links the two columns, and
that is the whole point.
Two commands are enough day to day. enable creates the symbolic link that
will start the service at boot, enable --now does both at once, and disableremoves the link.
Terminal window
sudosystemctlenablenginx# at the next boot
sudosystemctlenable--nownginx# at the next boot AND now
sudosystemctldisablenginx# will no longer start at boot
systemctlis-enablednginx# check
The output of enable depends on the distribution, and it is confusing the
first time. On RHEL, systemd prints the symbolic link it created. On
Debian and Ubuntu, a package that still ships a SysV script (nginx does)
first triggers a synchronisation, and the message talks about SysV without
mentioning any link:
Synchronizing state of nginx.service with SysV service script with /usr/lib/systemd/systemd-sysv-install.
In every case, the only check that counts is systemctl is-enabled nginx,
which must answer enabled. Do not trust the message printed, trust the
state.
Mask a service for good
disable prevents automatic start-up, but leaves the door open to a manual
systemctl start, including one triggered by a dependency of another
service. mask closes the door: systemd creates a link to /dev/null, and
nothing can start the service any more.
Terminal window
sudosystemctlmasknginx
Created symlink '/etc/systemd/system/nginx.service' → '/dev/null'.
Any start attempt then fails with an unambiguous message, and that is exactly
the behaviour you want for a service that must never run on a server, such
as Bluetooth, Avahi or CUPS:
Failed to start nginx.service: Unit nginx.service is masked.
Going back is done with sudo systemctl unmask nginx, which removes the link to
/dev/null and makes the service startable again. A
systemctl is-enabled nginx confirms it left the masked state.
mask has a limit that surprises: it only applies to a unit shipped by a
package, stored in /usr/lib/systemd/system/. For a unit you wrote yourself
in /etc/systemd/system/, the link to /dev/null would take the place of
your file, and systemctl refuses with
Failed to mask unit: File '/etc/systemd/system/mon-app.service' already exists
and an exit code of 1 (measured on Debian 13 and AlmaLinux 10). To neutralise such a
unit, disable --now stops it and removes it from boot; only deleting the
file, followed by a daemon-reload, prevents any start.
Create a service unit
Running a business application as a systemd service brings three things a
nohup will never give: automatic restart after a crash, start-up at
boot and centralised logs in the journal. The unit file lives in
/etc/systemd/system/ (the directory reserved for the administrator, which
takes precedence over the units shipped by packages).
A unit file reads as three blocks, and each one answers a different question:
[Unit] describes the service and its dependencies, [Service] says how
to start the process, [Install] says when to enable it at boot.
Remember the trap that follows: without an [Install] section, the service is
static and systemctl enable will have no effect.
Create the unit file with the built-in editor, which handles the path and
the permissions for you:
Terminal window
sudosystemctledit--full--forcemon-app.service
Describe the service, explicitly naming the unprivileged user that will
run it:
Enable and start the service. systemctl edit already reloaded the
systemd configuration when the editor closed; daemon-reload remains
mandatory as soon as the file is written another way, by a cp or a
deployment tool:
Terminal window
sudosystemctldaemon-reload# useless after systemctl edit, mandatory after a cp
sudosystemctlenable--nowmon-app.service
Really check that the service stays up, letting a few seconds pass:
Terminal window
sleep3 && systemctlis-activemon-app.service
The output must show active. If it shows failed,
journalctl -xeu mon-app.service gives the reason.
Forgetting daemon-reload after changing a unit file without going through
systemctl edit is the most frequent mistake: systemd keeps using the
version in memory, and your changes seem to have no effect.
The three sections
A unit file always reads the same way, which makes any unknown unit
readable in thirty seconds once you know the role of each section.
Section
Role
[Unit]
Description, dependencies (After=, Wants=, Requires=), start-up order
[Service]
The type, the command, the user, the restart policy
[Install]
What happens on enable: the target that will pull this service in at boot
Swipe to see more
A unit without an [Install] section is precisely a static unit: it
cannot be enabled, only pulled in by a dependency.
Choose the right Type
The Type= directive tells systemd how to know that the service has
started. It is the most misunderstood setting of all of systemd, and the
wrong choice produces false diagnoses.
Type
Behaviour
When to use it
simple
The process started by ExecStart is the service; systemd considers it started as soon as it forks
The default when ExecStart= is set, to avoid in favour of exec
exec
Like simple, but systemd waits for exec() to succeed
The right default for a foreground binary (systemd 240 and later)
forking
The process detaches, systemd follows the child process
Traditional daemons with a PIDFile=
oneshot
Runs a command then ends
Maintenance scripts, one-off tasks
notify
The service tells systemd it is ready through sd_notify()
Compatible applications, ideal for start-up order
idle
Waits for the other jobs to finish
Rare cases, console output
Swipe to see more
The trap: Type=simple does not check that your binary exists
Here is the behaviour almost nobody explains, and which costs hours of
debugging. With Type=simple, systemd considers the service started as soon
as it has forked, so before it even tried to execute the binary. The
official documentation says it in black and white: systemctl startreports
success even if the service binary cannot be invoked. Result: the command
returns with an exit code of 0, and the service is already dead. Run right
after, is-activecan still answer active: systemd switches the unit to
failed only after reaping the death of the child process, a window of a few
milliseconds.
Two identical units, a single detail that changes, and two opposite behaviours.
The first declares Type=simple, the second Type=exec, and both point to a
binary that does not exist:
[Service]
Type=simple
ExecStart=/opt/demo/absent
Terminal window
systemctlstartt-simple; echo"exit code of start = $?"; systemctlis-activet-simple
exit code of start = 0
active
The start is a success for the shell, and the service declares itself
active. With Type=exec, systemd waits for the result of exec() and
refuses to lie:
Terminal window
systemctlstartt-exec; echo"exit code of start = $?"; systemctlis-activet-exec
Job for t-exec.service failed because the control process exited with error code.
See "systemctl status t-exec.service" and "journalctl -xeu t-exec.service" for details.
exit code of start = 1
failed
The consequence is very concrete. A deployment script that tests $? after
a systemctl start believes it succeeded while the application is dead, and
the pipeline turns green. A systemctl status run a second too early shows a
perfectly misleadingactive (running), with a Main PID in double
parentheses that betrays a process that never managed its exec():
Active: active (running) since Sun 2026-07-12 16:54:43 UTC; 11ms ago
Main PID: 551 ((app.py))
Restart automatically after a crash
A service that dies at three in the morning must get back up on its own. The
Restart= directive handles this case, and the StartLimit* directives
prevent the infinite restart loop that would hide a real problem.
Directive
Value
Effect
Restart
on-failure
Restarts if the exit code is non-zero, or after a signal, a timeout or the watchdog
Restart
always
Always restarts, except after an explicit systemctl stop
Restart
no
Never restarts (the default)
RestartSec
5s
Waits 5 seconds between two attempts
StartLimitBurst
3
[Unit] section: beyond 3 attempts in the window, systemd gives up
StartLimitIntervalSec
60s
[Unit] section: the window in which attempts are counted
Swipe to see more
Restart=on-failure combined with RestartSec=5s is the setting that the
systemd.service(5) manual recommends for a long-running service. The
two StartLimit* directives live in [Unit], not in [Service]: put in the
wrong place, StartLimitIntervalSec= is ignored with a mere warning
(Unknown key 'StartLimitIntervalSec' in section [Service], ignoring, measured
on systemd 257 and 239), and your counting window does not exist.
Restart=always has a nasty side effect: it also restarts a service whose
configuration is broken, which fills the journal and delays the diagnosis.
Diagnose a failing service
When a service is in failed, the cause is almost always written in black and
white in the journal. You still have to read it in the right place. This
four-step workflow solves the majority of cases.
Read the status, which already shows the last lines of the journal:
Terminal window
systemctlstatusmon-app.service
Open the full journal of the unit, with the explanations of systemd:
Terminal window
journalctl-xeumon-app.service
The -x option adds contextual explanations, -e jumps to the end, -u
filters on the unit.
Spot the failure code. On a service whose ExecStart= points to a
missing file, the journal is explicit:
demo.service: Main process exited, code=exited, status=203/EXEC
demo.service: Failed with result 'exit-code'.
The 203/EXEC code is by far the most frequent: the executable is
missing, not executable, or its path has a typo. An ls -l on the
path of ExecStart= settles it in one second.
Fix, reload, restart, in this order:
Terminal window
sudosystemctldaemon-reload
sudosystemctlrestartmon-app.service
sleep3 && systemctlis-activemon-app.service
The most common failure codes form a vocabulary to know by heart:
203/EXEC for an invalid path, 217/USER for a User= that does not
exist, 200/CHDIR for a missing WorkingDirectory=, 226/NAMESPACE for a
hardening directive that is too strict. systemd-analyze exit-status lists
them all.
Harden an exposed service
A service reachable from the network should not be able to read /home, load a
kernel module or gain new privileges. systemd provides sandboxing
directives that go into the [Service] section without changing a single line
of the application. These four already cover most of the risk and rarely break
anything:
[Service]
# forbids any privilege escalation (suid)
NoNewPrivileges=true
# the whole file system read-only, except the data directory
ProtectSystem=strict
ReadWritePaths=/var/lib/mon-app
# /home, /root and /run/user invisible
ProtectHome=true
# an isolated /tmp, invisible to other services
PrivateTmp=true
The comments sit on their own line, and this is not a matter of style:
systemd accepts no end-of-line comment. Written
NoNewPrivileges=true # forbids…, the directive is rejected
(Failed to parse NoNewPrivileges=…, ignoring) then ignored, and the
service runs without the protection you think you set. Measured on the four
directives above, on systemd 257 and 239: systemd-analyze verify shows the
error, the start of the service says nothing.
The command systemd-analyze security mon-app.service rates the service from
0 to 10 (the lower the score, the better) and lists the missing protections
one by one. It is the fastest way to measure the effect of a directive: rate
before, add, rate after.
Troubleshooting
The symptoms below cover almost all the support calls about a systemd service.
The right-hand column gives the action, not a lead.
Symptom
Probable cause
Solution
systemctl start succeeds but the service is dead
Type=simple does not check exec()
Switch to Type=exec, or check with sleep 3 && systemctl is-active <service>
Service failed with 203/EXEC
Missing binary, wrong path or not executable
ls -l on the path of ExecStart=, then chmod +x if needed
enable prints "The unit files have no installation config" and returns 0
No [Install] section in the unit
Normal if the service is pulled in by a dependency; otherwise add [Install] and WantedBy=
A hardening directive has no effect
End-of-line comment, the directive is ignored
systemd-analyze verify <file>, comment on its own line
mask answers already exists
The unit was written in /etc/systemd/system/
disable --now, or delete the file: mask only targets package units
The service runs but disappears after reboot
Never enabled at boot
sudo systemctl enable <service> then check is-enabled
Permission denied in the journal
User= without rights on the files
Check the owner of the files and the hardening directives
Swipe to see more
Practice
Writing a unit while reading a guide and making it stand on a real system are
two different exercises. In a disposable VM, you write a complete
.service file, load it with daemon-reload, enable it with
enable --now and check its real state with is-active and the
journal.
Key points
list-units lists what is loaded in memory, list-unit-files what is installed on disk. A service installed but never started does not appear in list-units without --all: 14 loaded units against 119 installed files on the demonstration server.
For an audit, the right command is systemctl list-unit-files --type=service --state=enabled: it says what will start at the next boot.
enabled and active are independent: one talks about the next boot, the other about now.
Type=simple lies. On a missing binary, systemctl start returns 0 while the service is already dead in 203/EXEC (and is-active can answer active for a short moment). Declare Type=exec, or check the state a few seconds after the start.
203/EXEC is the most frequent failure code: invalid ExecStart= path or non-executable file.
After changing a unit file outside systemctl edit, daemon-reload is mandatory, otherwise systemd keeps its cached version.
reload rereads the configuration without cutting connections, restart kills the process: prefer reload when the unit declares an ExecReload=.
mask is stricter than disable: it makes a package service impossible to start, even through a dependency.
No end-of-line comment in a unit file: the directive is ignored. systemd-analyze verify reports it.
Knowledge check
Check that the essentials of this guide stuck. The questions cover only what
has just been explained here.
Knowledge check
Check what you have learned with this interactive quiz
6 questions
6 min
70% to pass
How it works
The timer starts when you click Start
Multiple choice, true/false and short answer questions
You can move between questions
Detailed results are shown at the end
Starts the quiz and the timer
⏱️--:--
0 / 6
Review
(0/0)
--:--
0%
0Correct
0To review
0:00Time
-Level
Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Continue the course: Master the Linux system logs with journalctl
A new full quiz with random questions
Work again only on the questions you missed
Frequently asked questions
systemd is the init and service management system used by most modern Linux distributions (Ubuntu, Debian, RHEL, Fedora, Arch...).
Why systemd?
Aspect
SysVinit (old)
systemd (modern)
Start-up
Sequential (slow)
Parallel (fast)
Dependencies
Manual scripts
Automatic
Logging
Scattered files
Centralised (journalctl)
Supervision
Cron/scripts
Built in (watchdog)
Isolation
Limited
cgroups, namespaces
Main components
systemctl: service management
journalctl: reading the logs
systemd-analyze: boot analysis
Units: services, timers, sockets, targets...
Check that systemd is running
# Check PID 1
ps -p 1 -o comm=
# Must print: systemd
# Version
systemctl --version
Here are the systemctl commands used most day to day:
Manage a service
# Start a service
sudo systemctl start nginx.service
# Stop a service
sudo systemctl stop nginx.service
# Restart (stop + start)
sudo systemctl restart nginx.service
# Reload the configuration (without stopping)
sudo systemctl reload nginx.service
# Check the state
systemctl status nginx.service
Automatic start-up
# Enable at boot
sudo systemctl enable nginx.service
# Disable at boot
sudo systemctl disable nginx.service
# Enable AND start right away
sudo systemctl enable --now nginx.service
List and search
# Services loaded in memory (not those never started)
systemctl list-units --type=service
# Every service installed on disk
systemctl list-unit-files --type=service
# Active services only
systemctl list-units --type=service --state=active
# Failed services
systemctl list-units --type=service --state=failed
# Search for a service
systemctl list-unit-files | grep docker
# Reload the configuration
sudo systemctl daemon-reload
# Enable at boot
sudo systemctl enable mon-app.service
# Start
sudo systemctl start mon-app.service
# Check
systemctl status mon-app.service
Sections explained
Section
Role
[Unit]
Metadata, description, dependencies
[Service]
How to run the service
[Install]
When to start (target)
These commands have very different roles:
Comparison
Command
Action
Immediate effect
At reboot
start
Starts the service
Yes
No
stop
Stops the service
Yes
No
restart
Stops then starts again
Yes
No
reload
Reloads the config
Yes (without interruption)
No
enable
Enables at boot
No
Yes
disable
Disables at boot
No
Yes
Use cases
# I want to start a service now
sudo systemctl start nginx
# I want it to start automatically at boot
sudo systemctl enable nginx
# I want both at once
sudo systemctl enable --now nginx
# I changed the nginx config, I want to reload without cutting the service
sudo systemctl reload nginx
# The service crashed, I want to start it again
sudo systemctl restart nginx
Watch out
reload only works if the service supports it (nginx, Apache...). Otherwise, use restart.
When a service fails, here is the diagnosis method:
Step 1: check the state
systemctl status mon-service.service
Look for:
Active: failed (red)
The exit code (status=1/FAILURE)
The last log lines
Step 2: read the full logs
# Logs of the service
journalctl -u mon-service.service
# Recent logs only
journalctl -u mon-service.service -n 50
# Follow in real time
journalctl -u mon-service.service -f
# Errors only
journalctl -u mon-service.service -p err
# Run the ExecStart command by hand
sudo -u www-data /opt/mon-app/venv/bin/python app.py
Frequent errors
Symptom
Probable cause
code=exited, status=203/EXEC
Invalid ExecStart path
code=exited, status=217/USER
User does not exist
code=exited, status=200/CHDIR
WorkingDirectory does not exist
code=killed, signal=KILL
OOM Killer (memory)
journalctl is the tool to read systemd logs.
Essential commands
# Logs of a specific service
journalctl -u nginx.service
# The last 100 lines
journalctl -u nginx.service -n 100
# Follow in real time (like tail -f)
journalctl -u nginx.service -f
# Since the last boot
journalctl -u nginx.service -b
# Since a date
journalctl -u nginx.service --since "2026-01-08 10:00"
# Between two dates
journalctl -u nginx.service --since yesterday --until today
# JSON format (for parsing)
journalctl -u nginx.service -o json
# Short format (one line per entry)
journalctl -u nginx.service -o short
# With precise timestamps
journalctl -u nginx.service -o short-precise
Disk space management
# Size of the logs
journalctl --disk-usage
# Clean up (keep 7 days)
sudo journalctl --vacuum-time=7d
# Clean up (keep 500 MB max)
sudo journalctl --vacuum-size=500M
A systemd target is a group of services that defines a state of the system (the equivalent of SysVinit runlevels).
Common targets
Target
Equivalent
Description
poweroff.target
runlevel 0
System shutdown
rescue.target
runlevel 1
Maintenance mode (single user)
multi-user.target
runlevel 3
Multi-user mode (no GUI)
graphical.target
runlevel 5
Graphical mode (with GUI)
reboot.target
runlevel 6
Reboot
Commands
# See the current target
systemctl get-default
# Change the default target (server without GUI)
sudo systemctl set-default multi-user.target
# Change the default target (desktop with GUI)
sudo systemctl set-default graphical.target
# Switch to a target right away
sudo systemctl isolate rescue.target
In a .service file
[Install]
WantedBy=multi-user.target
Means: "This service will be started when the system reaches multi-user mode."
Masking a service (mask) is stricter than disabling it (disable).
mask vs disable
Action
Effect
disable
The service does not start automatically at boot, but can be started by hand
mask
The service is fully blocked, impossible to start even by hand
Commands
# Mask a service
sudo systemctl mask dangerous-service.service
# Check (shows "masked")
systemctl status dangerous-service.service
# Try to start it (will fail)
sudo systemctl start dangerous-service.service
# Error: Unit dangerous-service.service is masked.
# Unmask
sudo systemctl unmask dangerous-service.service
Use cases
Prevent a vulnerable service from starting by accident
Turn off unwanted default services (cups, avahi...)
Conflicts between incompatible services
How it works
mask creates a symbolic link to /dev/null, which makes the service unusable. That is why it only applies to a unit shipped by a package: for a unit you wrote in /etc/systemd/system/, the link would replace your file, and systemctl refuses with already exists.
systemd uses cgroups to limit the resources of services.
In the .service file
[Service]
# Memory limit (max 512 MB)
MemoryMax=512M
# Throttling beyond 400 MB
MemoryHigh=400M
# CPU limit (50% of one core)
CPUQuota=50%
# Maximum number of processes
TasksMax=100
# I/O weight (from 1 to 10000, 100 by default)
IOWeight=200
Apply temporarily (without changing the file)
# Limit memory to 256 MB
sudo systemctl set-property mon-service.service MemoryMax=256M
# Limit CPU to 25%
sudo systemctl set-property mon-service.service CPUQuota=25%
Check the current limits
systemctl show mon-service.service | grep -E 'Memory|CPU|Tasks'
Full example (greedy service)
[Service]
ExecStart=/opt/app/run.sh
# Strict limits
MemoryMax=1G
MemoryHigh=800M
# 2 cores at most
CPUQuota=200%
TasksMax=50
# If the kernel kills a process (OOM), stop the whole unit
OOMPolicy=kill
No end-of-line comment in a unit: MemoryHigh=400M # ... is rejected then ignored. systemd-analyze verify reports it.
systemd offers hardening options to isolate services.
Recommended security options
[Service]
# Run as a non-root user
User=www-data
Group=www-data
# Prevent privilege escalation
NoNewPrivileges=true
# Read-only file system
ProtectSystem=strict
ProtectHome=true
# Private /tmp directory
PrivateTmp=true
# Kernel settings, modules and cgroups read-only or out of reach
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
# Filter system calls
SystemCallFilter=@system-service
SystemCallArchitectures=native
# Restricted network (if not needed)
PrivateNetwork=true
# Limited capabilities
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE
systemd can restart failed services automatically.
Configuration in the .service file
[Unit]
# At most 5 starts per minute, otherwise give up
StartLimitBurst=5
StartLimitIntervalSec=60
[Service]
ExecStart=/opt/app/run
# Restart on failure
Restart=on-failure
# Delay between two restarts
RestartSec=5
The StartLimit* directives go in [Unit]: put in [Service], StartLimitIntervalSec= is ignored with a warning.
Restart options
Value
Restarts if...
no
Never (default)
on-success
Exit code = 0
on-failure
Code other than 0, signal, timeout
on-abnormal
Signal, timeout, watchdog
on-abort
Signal not caught by the program
always
Always (except systemctl stop)
Robust example
[Unit]
# At most 3 starts in 5 minutes
StartLimitBurst=3
StartLimitIntervalSec=300
[Service]
ExecStart=/opt/api/server
Restart=always
RestartSec=10
# Watchdog: the program must signal it is alive every 30 s (sd_notify)
WatchdogSec=30
Check the behaviour
# Kill the process to test the restart
sudo kill -9 $(pgrep -f mon-app)
systemctl status mon-app.service
systemd timers are a good replacement for cron for scheduled tasks.
# /etc/systemd/system/backup.timer
[Unit]
Description=Timer for the daily backup
[Timer]
# Every day at 3 in the morning
OnCalendar=*-*-* 03:00:00
# Catch up if the system was off
Persistent=true
# Add a random delay (avoids simultaneous load)
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
# Every day at midnight
OnCalendar=daily
# Every hour
OnCalendar=hourly
# Every Monday at 8 am
OnCalendar=Mon *-*-* 08:00:00
# Every 15 minutes
OnCalendar=*:0/15
List the active timers
systemctl list-timers
The Type defines how systemd detects that the service is "ready".
Main types
Type
Behaviour
Example
simple
The ExecStart process is the service, ready as soon as it forks
Historical default
exec
Like simple, but waits for exec() to succeed
The right choice for a foreground binary
forking
The process forks, the parent exits
Apache, traditional nginx
oneshot
Runs once and ends
Maintenance scripts
notify
The service signals when it is ready
dockerd
dbus
Ready when the D-Bus name is acquired
Desktop services
idle
Waits for the jobs to be finished
Login prompts
Examples
exec (to prefer over simple, which is the default): a missing binary makes systemctl start fail, where simple returns 0:
systemd manages dependencies through directives in the [Unit] section.
Main directives
Directive
Meaning
After=
Start after this service (order)
Before=
Start before this service
Requires=
Strong dependency: an explicit stop of the other stops this one; its failure at start-up only blocks this one with After=
Wants=
Weak dependency (if the other fails, go on)
BindsTo=
Very strong dependency: if the other stops or disappears, this one stops
Conflicts=
Cannot run at the same time
Examples
API that needs the database:
[Unit]
Description=My API
After=network.target postgresql.service
Requires=postgresql.service
# PostgreSQL starts before the API; if its start fails, the API does not start.
# A later crash of PostgreSQL does not stop the API: that would need BindsTo=.
Optional service (Redis as a cache):
[Unit]
Description=My API with an optional cache
After=network.target redis.service
Wants=redis.service
# If Redis is not there, the API starts anyway
Incompatible services:
[Unit]
Description=Apache
Conflicts=nginx.service
# Apache and nginx cannot run together
View the dependencies
# Dependencies of a service
systemctl list-dependencies nginx.service
# Reverse dependencies (who depends on...)
systemctl list-dependencies --reverse network.target