Soft2Soft Ops Practical knowledge base
Linux

Systemd Service Won’t Start: How to Read status and journalctl to Find the Cause

16 views
linux systemd journalctl devops

Diagnostic workflow for a failed unit

If a service enters the failed state, do not change permissions at random or disable security mechanisms. First determine where the failure occurred: systemd could not prepare the environment, the command in ExecStart did not run, the application exited with an error, or a dependency failed to start.

  1. Check the state with systemctl status.
  2. Read the full journal with journalctl -u.
  3. Check the loaded unit file, paths, user, and environment.
  4. Run the command again as the service user.
  5. After fixing the problem, validate the configuration, run daemon-reload, and restart the service.

Step 1. Read systemctl status

sudo systemctl status myapp.service --no-pager -l

The important lines are Loaded, Active, Process, and the latest journal messages. Loaded shows the path to the unit file that was read. Active shows the state and result, for example failed (Result: exit-code). The process line contains the command, PID, and the code=/status= fields. The status output is limited, so use it as a brief summary rather than a replacement for the journal. :contentReference[oaicite:0]{index=0}

After fixing the error, reset the failed-start limit if necessary:

sudo systemctl reset-failed myapp.service

Step 2. Find the cause with journalctl

sudo journalctl -u myapp.service -b -n 100 --no-pager
sudo journalctl -u myapp.service --since "10 minutes ago" --no-pager

The -u option filters entries by unit, -b limits them to the current system boot, -n limits the number of lines, and --since separates the latest attempt from older errors. Look for the first meaningful message: “file not found,” “permission denied,” “user does not exist,” “failed to change directory,” a configuration error, or a message from the application itself. :contentReference[oaicite:1]{index=1}

The Failed to start line describes the final result; the actual cause is usually shown above it.

How to interpret exit codes

code=exited, status=1 usually means that the program started but returned a nonzero exit code itself. systemd status codes help identify a failure that occurred before the process started normally:

SymptomLikely causeWhat to check
203/EXECThe command could not be executedPath, file existence, execute permission, file format, and shebang
200/CHDIRThe working directory is inaccessibleWorkingDirectory and path permissions
217/USERThe user could not be appliedWhether User exists
216/GROUPThe group could not be appliedWhether Group exists
status=1 or another application exit codeAn error occurred inside the programArguments, configuration, environment, and application logs
start-limit-hitToo many failed start attemptsThe first error, then reset-failed

These codes refer to the systemd process preparation stages. :contentReference[oaicite:2]{index=2}

Step 3. Check the loaded configuration

sudo systemctl cat myapp.service
sudo systemctl show myapp.service -p FragmentPath -p DropInPaths
sudo systemd-analyze verify /etc/systemd/system/myapp.service

systemctl cat shows the main file and drop-in settings, while systemd-analyze verify detects syntax errors and unknown directives.

ExecStart and paths

For predictable startup behavior, specify absolute paths to the interpreter, executable, and application files. systemd can resolve a simple program name through its own search path, but it does not use the interactive environment of your SSH shell. As a result, a command that works manually may not be found when started as a service. :contentReference[oaicite:3]{index=3}

ExecStart=/opt/myapp/venv/bin/python /opt/myapp/app.py

Pipelines, redirections, and && are not processed by a shell automatically. Prefer starting the program directly. When a shell is actually required, invoke it explicitly:

ExecStart=/bin/sh -c '/usr/bin/example --check && /usr/bin/example --run'

User, Group, and WorkingDirectory

Check that the account exists and that the user can access the executable, configuration, working directory, and data directory:

getent passwd myapp
getent group myapp
namei -l /opt/myapp/venv/bin/python
sudo -u myapp test -x /opt/myapp/venv/bin/python
sudo -u myapp test -r /etc/myapp/myapp.env
sudo -u myapp test -w /var/lib/myapp

The user needs traverse permission on every parent directory. WorkingDirectory must exist before startup and be accessible to the user.

Do not use chmod 777 or disable SELinux, AppArmor, or unit-file isolation. Identify the specific inaccessible resource and grant only the minimum required permissions to the owner or group. For a mandatory access control system, fix the label, profile, or allow rule.

EnvironmentFile and variables

Check the path, read permissions, and file format. EnvironmentFile contains variable assignments, not a full interactive shell script. Do not rely on loading .bashrc, aliases, or command substitution.

EnvironmentFile=/etc/myapp/myapp.env
Environment="APP_MODE=production"
APP_PORT=8080
APP_DATA=/var/lib/myapp

The EnvironmentFile=-/etc/myapp/myapp.env form makes the file optional; use it only intentionally.

Example unit file

[Unit]
Description=MyApp application service
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/opt/myapp/venv/bin/python /opt/myapp/app.py
Restart=on-failure
RestartSec=5s
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

The user, directories, virtual environment, and environment file must exist beforehand.

Step 4. Run the command as the service user

Test the command not as root, but with the same permissions and working directory as the service:

sudo -u myapp -H sh -c '
  cd /opt/myapp || exit 1
  set -a
  . /etc/myapp/myapp.env
  set +a
  exec /opt/myapp/venv/bin/python /opt/myapp/app.py
'

This test reveals permission, directory, environment variable, and configuration problems, although it does not emulate every systemd restriction.

daemon-reload and dependencies

After changing the unit file, reload the configuration, restart the service, and immediately check the latest entries:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service --no-pager -l
sudo journalctl -u myapp.service -b -n 50 --no-pager

daemon-reload does not restart the application, while restart without a reload may use the previously loaded configuration.

If a dependency fails, check it separately:

sudo systemctl list-dependencies myapp.service
sudo systemctl show myapp.service -p Requires -p Wants -p After
sudo systemctl status required.service --no-pager -l
sudo journalctl -u required.service -b --no-pager

After defines ordering but does not pull in another unit by itself. Wants creates a weak dependency, while Requires creates a stronger one. Do not hide the problem by adding a startup delay: identify the resource that must be ready and the unit responsible for it.

Checklist before restarting

  • Loaded points to the expected unit file.
  • The journal contains the primary error, not only the final Failed to start message.
  • ExecStart contains the correct command, arguments, and paths.
  • The executable exists, has a valid shebang, and is accessible to the service user.
  • User and Group exist.
  • WorkingDirectory exists and can be traversed.
  • EnvironmentFile is readable and its path contains no typos.
  • Permissions are limited to the minimum required, without chmod 777.
  • Dependency units are active, and startup ordering is defined intentionally.
  • After editing, systemd-analyze verify, daemon-reload, and restart have been run.
  • After startup, both status and the latest journal entries have been checked.