Runbook: Investigate a systemd service that will not start
1 · Prerequisites
Confirm every item is in place before any state change.
2 · Pre-checks
Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.
- · Confirm the unit name exactly, including the suffix (systemctl list-units --all "<pattern>*")
- · Confirm the host itself is healthy: disk not full (df -h /, df -i /), load reasonable, no OOM kills
- · Confirm nobody else is already working the incident
- · Record the current state before changing anything (systemctl status, journal excerpt)
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Read the unit state and the last exit code with systemctl status --no-pager -l
- 2Read the failure itself in the journal for that boot: journalctl -u <unit> -b --no-pager
- 3Classify the failure: config syntax, missing dependency, permissions/MAC denial, resource exhaustion, or crash loop
- 4Validate the application config with its own checker before restarting anything
- 5Validate the unit file with systemd-analyze verify
- 6Fix the single root cause identified; do not stack changes
- 7Restart the unit and watch it, do not fire and forget
- 8Reset the failure counter if start-limit throttling was hit
- 9Record the cause and the fix
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓systemctl is-active <unit> returns active
- ✓The service answers its own health check, not just "the process exists"
- ✓journalctl -u <unit> --since "5 min ago" shows a clean start with no repeating errors
- ✓systemctl show <unit> -p NRestarts shows the counter is no longer climbing
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Restore the configuration file from the backup taken before the edit
- ↶Re-run the application config checker to confirm the restored file parses
- ↶Restart the unit and confirm it returns to its previous state
- ↶If the failure followed a package upgrade, downgrade to the previous version and pin it pending investigation
6 · Escalation
When the runbook isn't enough, contact:
- · Application-level crash with a clean config and clean host: escalate to the application team with the journal excerpt and the exit code
- · Repeatable segfault or kernel taint: escalate to the platform or vendor team
- · MAC denial you cannot resolve without a policy change: escalate to the security team, never disable enforcement to close the incident
- · The unit is a cluster resource: stop and use the Pacemaker runbook instead, do not restart it by hand
A service that will not start is the most common ticket in
any Linux estate. It is also the one most often closed
without a cause, because systemctl restart sometimes works
and everybody moves on. This runbook finds the cause first.
When to use this runbook
- A unit is
failed, oractivatingand never reachingactive. - A unit is flapping: it starts, dies, and restarts.
- A unit reports
activebut the service does not answer.
Do not use this runbook for a unit managed by Pacemaker. Restarting a cluster resource by hand fights the cluster and can trigger a fence. Use the Pacemaker runbook.
Inputs
- The exact unit name.
- What changed: a deploy, a package upgrade, a config management run, a certificate renewal, a reboot.
- Whether the service ever worked on this host.
Step 1: Read the state, including the exit code
systemctl status nginx --no-pager -l
systemctl show nginx -p Result -p ExecMainStatus -p NRestarts -p ActiveStateThree fields decide where you go next.
Result—exit-codemeans the process ran and returned non-zero.timeoutmeans it never signalled readiness.oom-killmeans the kernel killed it.protocolusually means the wrongType=.ExecMainStatus— the exit status. Most daemons use their own codes; 203 is systemd’s own “could not execute” and almost always means a badExecStartpath or a missing execute bit.NRestarts— if this is climbing, you have a crash loop, not a one-off failure.
Step 2: Read the actual error
systemctl status shows the last ten lines. The cause is
usually above them.
# This boot, this unit, in full
journalctl -u nginx -b --no-pager
# Just the failure window, with priority filtering
journalctl -u nginx --since "20 min ago" -p warning --no-pager
# Follow the next start attempt live
journalctl -u nginx -fIf the unit is flapping, -f in one window and the restart
in another is the fastest way to see the first error rather
than the tenth.
Step 3: Classify before you fix
Match the journal against these five shapes. Each has a different fix and a different blast radius.
| Shape | What you see | Where to go |
|---|---|---|
| Config syntax | The daemon’s own parse error, naming a file and line | Step 4 |
| Bad unit file | 203/EXEC, “Neither a valid executable name nor an absolute path”, “more than one ExecStart” | Step 5 |
| Dependency | Job ... failed with result 'dependency', or a mount/network unit failed first | Step 6 |
| Permission or MAC | Permission denied on a path the daemon owns, or apparmor="DENIED" / avc: denied | Step 7 |
| Resource | oom-kill, Too many open files, No space left on device | Step 8 |
Fix one shape at a time. Stacking three speculative changes means you cannot say afterwards which one mattered, and the next occurrence starts from zero.
Step 4: Validate the application config
Every serious daemon has a checker. Use it before the restart, while the old process is still serving.
sudo nginx -t
sudo apachectl configtest
sudo sshd -t
sudo named-checkconf
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo chronyd -Q -f /etc/chrony.confThis is the difference between a five-second fix and an outage. A restart with a broken config kills the running process and then fails to start a new one. The checker tells you first, at no cost.
Step 5: Validate the unit file
systemd-analyze verify /etc/systemd/system/myapp.service
# See the effective unit after every drop-in is merged
systemctl cat myapp.service
# Which drop-ins exist and where they came from
systemd-delta --type=extended | grep -i myappThe three faults that account for most of these:
203/EXEC. TheExecStartbinary does not exist, is not executable, or has a bad interpreter line. Check withls -landhead -1on the script.- More than one
ExecStart. Only legal forType=oneshot, where all lines run in sequence. For any other type the unit refuses to load and the job silently never runs. - Wrong
Type=. A daemon that forks underType=simplelooks like an instant exit. A daemon that stays in the foreground underType=forkingtimes out.
sudo systemctl daemon-reload
sudo systemctl restart myapp.serviceStep 6: Follow the dependency chain
result 'dependency' means this unit never ran. Something it
required failed first, and that is the unit to investigate.
systemctl list-dependencies myapp.service --all
systemctl --failed --no-pager
systemctl list-units --state=failed --no-pager
# Ordering is not the same as requirement - check both
systemctl show myapp.service -p Requires -p After -p Wants -p BindsToAfter= only orders; it does not require. A unit with
After=network-online.target but no Wants= will start
without waiting, and a service that binds an address before
the address exists fails in a way that looks random.
Step 7: Permissions and mandatory access control
# AppArmor
sudo journalctl -k --since "20 min ago" | grep -i 'apparmor="DENIED"'
sudo aa-status | head
# SELinux
sudo ausearch -m avc -ts recent
sudo sealert -a /var/log/audit/audit.log 2>/dev/null | head -40
# Plain filesystem permissions on what the unit actually touches
sudo -u www-data test -r /etc/myapp/myapp.conf && echo readable || echo DENIED
namei -l /etc/myapp/myapp.confnamei -l walks every component of the path. A daemon
denied at /etc/myapp because the directory is 0700 looks
identical in the journal to one denied at the file.
Step 8: Resource exhaustion
df -h /
df -i /
free -h
journalctl -k --since today | grep -i -E 'out of memory|oom-kill|killed process'
# What the unit was actually granted
systemctl show myapp -p LimitNOFILESoft -p LimitNOFILE -p MemoryMax -p MemoryCurrent
cat /proc/$(systemctl show myapp -p MainPID --value)/limits 2>/dev/nullTwo exhaustion faults look like a service bug and are not:
- Full filesystem or exhausted inodes. The daemon cannot write its PID file, socket, or log. See the filesystem-full runbook.
Too many open files. The softRLIMIT_NOFILEis too low for the connection count. Fix it withLimitNOFILE=on the unit, not withnofile unlimitedinlimits.conf.
Step 9: Restart, and watch
# Reload if the daemon supports it - no dropped connections
sudo systemctl reload nginx
# Otherwise restart, and watch the result rather than assuming it
sudo systemctl restart nginx
systemctl is-active nginx
journalctl -u nginx -n 30 --no-pagerIf the unit hit systemd’s start-limit throttle
(start request repeated too quickly), the counter must be
cleared or systemd will refuse to try again:
sudo systemctl reset-failed nginx
sudo systemctl start nginxreset-failed clears a counter. It fixes nothing. If you
find yourself running it more than once, the crash loop is
the incident.
Step 10: Verify against the service, not the process
# Is it listening where you think
sudo ss -lntp | grep -E 'nginx|:80|:443'
# Does it answer
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost/healthz
# Has it stayed up
systemctl show nginx -p NRestarts -p ActiveEnterTimestampCommon patterns
| Symptom | Likely cause | Resolution |
|---|---|---|
203/EXEC | Bad ExecStart path, missing execute bit, bad shebang | ls -l, head -1, fix the path |
status=1 immediately | Application config error | Run the daemon’s own config checker |
Result: timeout | Wrong Type=, or readiness never signalled | Match Type= to the daemon’s behaviour |
result 'dependency' | A required unit failed first | systemctl --failed, fix that unit |
| Starts then dies in seconds | Crash loop; port in use, missing data dir, bad credential | Read the first failure, not the last |
Permission denied on its own files | Ownership drift, or a MAC denial | namei -l, then check AppArmor/SELinux |
Too many open files | LimitNOFILE too low | Set LimitNOFILE= on the unit |
| Active but not serving | Type=simple marks active on fork | Health-check the service itself |
Knowledge check
Knowledge check · 4 questions
Q1. An nginx config change is deployed and someone runs `systemctl restart nginx`. The site goes down. What should have happened first?
Q2. systemctl status shows `active (running)` but every request to the service fails. What is the most likely explanation?
Q3. When AppArmor or SELinux blocks a service, running `setenforce 0` or `aa-disable` is an acceptable way to restore production quickly.
Q4. Which of these are read-only diagnostics safe to run during an incident? Select all that apply.
Passing score: 75%. Answers are checked in this browser.