Common Systemd Configuration Errors and How to Fix Them

Fix common systemd unit file mistakes: bad paths, wrong service types, missing environment, permissions, and dependency ordering.

Common Systemd Configuration Errors and How to Fix Them

Systemd configuration errors usually look more dramatic than they are. A service refuses to start, a deployment rolls back, or a boot hangs on a unit name you barely remember creating. Then the real cause turns out to be a missing slash in ExecStart=, a process running as the wrong user, or a unit file change that never reached the systemd manager because nobody ran daemon-reload.

The fastest way through these problems is to treat the unit file as a contract. It tells systemd what process to run, which user to run it as, what needs to exist first, how readiness is reported, and what should happen after a crash. When one of those details is wrong, systemd is usually doing exactly what it was told. The work is finding the mismatch between what the application needs and what the unit file actually says.

1. Syntax and Pathing Errors in Unit Files

One of the most frequent causes of service failure is a simple typo or an incorrectly defined path within the unit file.

Incorrect or Non-Absolute Paths in Exec Commands

Systemd does not run your service from the same shell session you used for testing. It starts the process in a controlled environment, so assumptions about aliases, shell functions, virtualenv activation, and a custom PATH often fail. Use absolute paths for the executable in ExecStart= and be explicit about every directory or file the service needs.

The Error:

Using a command name without specifying its location.

[Service]
ExecStart=my-app-server --config /etc/config.yaml

If my-app-server is located in /usr/local/bin, systemd likely won't find it.

The Fix:

Always use the full, absolute path to the executable.

[Service]
ExecStart=/usr/local/bin/my-app-server --config /etc/config.yaml

Before configuring ExecStart=, verify the path with command -v my-app-server or which my-app-server. If the application lives in a language-specific location, such as a Python virtual environment under /opt/myapp/venv/bin/gunicorn, point at that binary directly instead of relying on activation scripts.

Typographical Errors and Case Sensitivity

Systemd configuration directives are case-sensitive and must be placed in the correct sections ([Unit], [Service], [Install]). Misspellings or incorrect capitalization will result in the service failing to load or exhibiting unexpected behavior.

Example Error:

[Service]
ExecStart=/usr/bin/python3 app.py
RestartAlways=true  ; Should be Restart=always

The Fix:

Use systemd-analyze verify <unit_file> before reloading the daemon. It will not catch every runtime mistake, but it catches many misspelled directives, invalid section placement, and parse errors before you waste time chasing application logs.

$ systemd-analyze verify /etc/systemd/system/my-service.service

2. Mismanaging Service Dependencies and Ordering

Dependencies define what resources a service needs, while ordering defines when those resources must be available.

Confusing Requires vs. Wants

These directives are used to define dependencies but handle failures differently:

  • Wants=: A weak dependency. If the wanted unit fails or doesn't start, the current unit will still attempt to start. Use this for non-critical dependencies.
  • Requires=: A strong dependency. If the required unit cannot be started, the current unit is also failed. If the required unit is explicitly stopped, the dependent unit is stopped too.

Relying on Requires without Proper Ordering

Defining a dependency, for example Requires=network.target, pulls the dependency into the transaction. It does not by itself create startup order, and network.target does not mean "the network is usable for outbound connections." If your service needs configured networking, use network-online.target and make sure the distribution's wait-online service is enabled when that behavior is required.

The Error:

A web server starts, but the database connection fails because the networking stack is still initializing.

The Fix: Using After= and Before=

To enforce ordering, you must use After= (or Before=). A common requirement is ensuring the network is fully up and configured before proceeding.

[Unit]
Description=My Web Application Service
Wants=network-online.target
After=network-online.target

[Service]
...

For most application services, pair dependency intent with ordering intent. Wants=postgresql.service says "please start PostgreSQL too." After=postgresql.service says "start me after PostgreSQL's start job finishes." They solve different problems.

Incorrect Service Type Management

Systemd services have several execution types, managed by the Type= directive. Misconfiguring this is a common cause of services starting momentarily and then immediately failing.

The Error: Misusing Type=forking

If your application is designed to run in the foreground and maintain a single main process, setting Type=forking tells systemd to expect old-style daemon behavior. That can lead to confusing results: systemd may wait for a parent process to exit, fail to identify the real main process, or mark the service active while the application is not actually ready.

The Fixes:

  1. For modern applications: Use Type=simple. This is the default and expects the ExecStart process to be the main process.
  2. For legacy applications that daemonize (fork): Set Type=forking and crucially, define the PIDFile= directive so systemd can track the child process that survived the fork.
[Service]
Type=forking
PIDFile=/var/run/legacy-app.pid
ExecStart=/usr/sbin/legacy-app

There is another common readiness trap: using Type=simple for an application that takes a long time to become usable. With Type=simple, systemd considers the service started as soon as the process is spawned. If another service starts immediately afterward and connects to it, you may see intermittent failures. For applications that can notify systemd when they are ready, Type=notify is cleaner. For applications that cannot, avoid pretending the unit is fully ready just because the process exists; use a real health check in the dependent application, socket activation, or an ExecStartPre= check when the prerequisite is simple enough to test.

Be careful with oneshot too. A Type=oneshot service is for a command that performs a task and exits, such as creating a directory, loading a firewall rule, or running a migration. If you use it for a long-running daemon, systemd will not supervise it the way you expect. If the command exits successfully and you want the unit to remain "active" for dependency purposes, add RemainAfterExit=yes; otherwise, dependent units may not see the state you intended.

3. Environmental and User Context Issues

Service failures often stem from the service running in a context different from what the application expects, usually related to permissions or environment variables.

Permission Denied or Missing Files

When testing an application manually, it typically runs under your user account with appropriate permissions. When run by systemd, it often defaults to the root user or the user specified in the unit file.

The Error:

Typical symptoms are blunt: Permission denied, No such file or directory, Failed to open log file, or an application-specific error saying it cannot create a socket, write a PID file, or read a configuration file. The unit may work when you run the command manually as root, then fail under User=app.

The Fix:

  1. Define a Non-Root User: Always specify a dedicated, low-privilege user and group for your service.

    [Service]
    User=www-data
    Group=www-data
    ...
    
  2. Check Ownership: Ensure the service's working directory, log files, and configuration files are owned by the specified User= and Group=.

    sudo chown -R www-data:www-data /var/www/my-app
    
  3. Check every path the service touches: Do not stop at the application directory. Check WorkingDirectory=, log directories, upload directories, cache directories, TLS key files, Unix sockets, and any path referenced by an environment file.

    sudo -u www-data test -r /etc/my-app/config.yml
    sudo -u www-data test -w /var/lib/my-app
    sudo -u www-data /usr/local/bin/my-app --check-config
    

If the service needs to bind to port 80 or 443, do not automatically run it as root. On many systems you can put a reverse proxy in front of it, use socket activation, or grant the binary the specific capability it needs. The right choice depends on the service, but a broad root process should not be the default answer.

One more permission detail catches people: parent directories need execute permission. A file can look readable, but the service still cannot reach it because /opt, /opt/myapp, or another parent directory blocks traversal for the service user. namei -l /opt/myapp/config.yml is useful because it shows permissions for each path component instead of only the final file.

Missing Environment Variables

Systemd services run in a minimal environment. Any crucial environment variables (like API keys, database connection strings, or custom library paths) must be explicitly passed.

The Fix: Using Environment= or EnvironmentFile=

For simple variables, use Environment=:

[Service]
Environment="APP_PORT=8080"
Environment="API_KEY=ABCDEFG"

For complex or numerous variables, use EnvironmentFile= pointing to a standard .env file:

[Service]
EnvironmentFile=/etc/default/my-app.conf

Keep secrets out of world-readable unit files. A unit file under /etc/systemd/system is usually readable by local users. If you put API keys directly in Environment=, assume they are exposed to anyone who can read unit files or inspect process metadata. Prefer a root-owned environment file with restrictive permissions, a secret manager, or systemd credentials on distributions that support them.

Also remember that EnvironmentFile= is not a shell script. Lines such as export APP_PORT=8080 or command substitutions like TOKEN=$(cat /run/token) are not interpreted the way they would be in Bash. Use plain assignments:

APP_PORT=8080
APP_ENV=production

If the application needs a login shell setup, that is usually a smell. Put the real environment into the unit file, the environment file, or the application's own configuration instead of depending on .bashrc.

For language runtimes, point systemd at the runtime you actually tested. A Python service should usually call the virtual environment binary directly, such as /opt/myapp/venv/bin/python or /opt/myapp/venv/bin/gunicorn. A Node service installed through a version manager may work in your terminal but fail under systemd because nvm or asdf modified only your interactive shell. In production units, explicit paths beat shell startup magic.

4. The Crucial Debugging Workflow

The most common configuration error is forgetting the crucial step between editing the unit file and attempting to restart the service.

Forgetting to Reload the Daemon

Systemd does not automatically monitor unit files for changes. After any modification to a file in /etc/systemd/system/, the systemd manager must be instructed to reload its configuration cache.

The Error:

You edit the file, run systemctl restart my-service, but the old configuration is still used.

The Fix: Run daemon-reload

Always execute this command immediately after saving a unit file change:

sudo systemctl daemon-reload
sudo systemctl restart my-service

If you edited a drop-in override with systemctl edit my-service, the same rule applies. The generated override is stored under /etc/systemd/system/my-service.service.d/, and systemd still needs to reload its unit cache before the new settings matter.

When a restart behaves strangely, inspect the exact merged unit that systemd sees:

systemctl cat my-service.service
systemctl show my-service.service -p FragmentPath -p DropInPaths -p User -p ExecStart

This catches a common mistake: editing a vendor unit in /usr/lib/systemd/system while an override in /etc/systemd/system still changes the setting, or editing a copy of a unit that is not the one systemd loaded.

Effective Use of Logging Tools

When a service fails, rely on the official tools for accurate diagnosis.

  1. Check Service Status: This gives you the immediate state, exit codes, and the last few log lines.

    systemctl status my-service.service
    
  2. Inspect the Journal: The journal holds the comprehensive output (stdout/stderr) of the service. Look for clues like "Permission denied" or "No such file or directory".

    # View recent logs specifically for your unit
    journalctl -u my-service.service --since '1 hour ago' 
    
    # View logs and follow output in real-time
    journalctl -f -u my-service.service
    

A Practical Troubleshooting Pass

When I review a broken unit, I usually make one pass in this order:

systemctl status my-service.service
journalctl -u my-service.service --since "15 minutes ago"
systemctl cat my-service.service
systemd-analyze verify /etc/systemd/system/my-service.service

Then I ask plain questions. Does ExecStart= point to a real executable? Can the configured User= run it? Does WorkingDirectory= exist? Are the environment variables present without relying on a shell? Is the Type= honest about how the process behaves? Are Wants= and After= both present when I need another unit to be started and ordered before this one?

After each edit, reload and test one thing:

sudo systemctl daemon-reload
sudo systemctl restart my-service.service
systemctl status my-service.service --no-pager

If the service still fails, resist the urge to keep changing the unit file blindly. The journal usually tells you whether the next problem is systemd configuration, application configuration, permissions, or a dependency that is failing on its own.