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.
- Check the state with
systemctl status. - Read the full journal with
journalctl -u. - Check the loaded unit file, paths, user, and environment.
- Run the command again as the service user.
- 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:
| Symptom | Likely cause | What to check |
|---|---|---|
203/EXEC | The command could not be executed | Path, file existence, execute permission, file format, and shebang |
200/CHDIR | The working directory is inaccessible | WorkingDirectory and path permissions |
217/USER | The user could not be applied | Whether User exists |
216/GROUP | The group could not be applied | Whether Group exists |
status=1 or another application exit code | An error occurred inside the program | Arguments, configuration, environment, and application logs |
start-limit-hit | Too many failed start attempts | The 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 777or 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
Loadedpoints to the expected unit file.- The journal contains the primary error, not only the final
Failed to startmessage. ExecStartcontains the correct command, arguments, and paths.- The executable exists, has a valid shebang, and is accessible to the service user.
UserandGroupexist.WorkingDirectoryexists and can be traversed.EnvironmentFileis 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, andrestarthave been run. - After startup, both
statusand the latest journal entries have been checked.