Systemd Service Unit File Tutorial

by Liam Foster
Systemd Service Unit File Tutorial

A systemd service unit file is a declarative configuration that tells your Linux system how to start, monitor, and stop a service. Instead of writing shell scripts, you write a unit file — a text file with key-value pairs — and systemd handles the rest: dependency ordering, restart logic, logging, socket activation, and privilege dropping. This is the standard service management layer on modern Linux distributions.

The Unit File Structure

Every systemd service unit file lives in /etc/systemd/system/ (or /usr/lib/systemd/system/ for packaged services) and follows INI syntax. A minimal unit file has three sections:

[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure

[Install]
WantedBy=multi-user.target

The [Unit] section declares metadata and dependencies. The [Service] section defines how to run the process. The [Install] section tells systemd where to link this unit into the boot sequence.

The Unit Section

Description is a human-readable label. It appears in systemctl status output and logs.

After=network.target orders this unit to start after the network is ready. This is a dependency ordering, not a hard requirement — if network.target fails, this unit still starts. Use Requires=network.target if you want a hard dependency (the service fails if network.target fails).

Before= works the opposite direction: this unit starts before the named target.

Wants= is a weak dependency: systemd tries to start the named unit, but doesn't fail this unit if it can't. Use Wants= for optional integrations.

ConditionPathExists=/path/to/file skips this unit entirely if the path doesn't exist. Other condition directives: ConditionFileNotEmpty, ConditionUser, ConditionVirtualization. Useful for conditional activation.

The Service Section

Type determines how systemd tracks the process:

  • simple (default): systemd assumes the process runs in the foreground and doesn't fork. The main PID is the process you started. Most modern applications use this.
  • forking: the process forks and exits; systemd waits for the parent to exit, then tracks the child. Older daemons use this pattern.
  • oneshot: the process runs to completion and exits. Useful for setup tasks or scripts. Set RemainAfterExit=yes if you want systemd to consider the unit "active" even after the process exits.
  • notify: the process sends a notification to systemd when it's ready. Requires the process to use sd_notify() or equivalent. Prevents race conditions in dependency chains.
  • dbus: similar to notify, but signals readiness by appearing on the D-Bus system bus.

ExecStart is the command to run. Use an absolute path. Arguments are space-separated; quote arguments with spaces. systemd does not invoke a shell, so no pipes, redirects, or shell expansions — write them explicitly if needed:

ExecStart=/bin/sh -c 'exec /usr/local/bin/myapp > /var/log/myapp.log 2>&1'

ExecStartPre and ExecStartPost run before and after ExecStart. Useful for setup and cleanup. If ExecStartPre fails, ExecStart doesn't run.

ExecStop is the command to stop the service. If not set, systemd sends SIGTERM to the main process. Set a custom stop command if your application needs graceful shutdown logic.

ExecReload runs when you call systemctl reload. Useful for configuration reloads without stopping the service.

Restart controls whether systemd restarts the service after it exits:

  • no (default): don't restart.
  • always: always restart, regardless of exit code.
  • on-failure: restart only if the exit code is non-zero or the process was killed by a signal.
  • on-abnormal: restart if killed by a signal or timeout.
  • on-watchdog: restart if the watchdog timer expires.

RestartSec=5 waits 5 seconds between restarts (default 100ms). Prevents restart loops from hammering resources.

StandardOutput and StandardError control where the process writes. Set to journal to send logs to the systemd journal (default for most services). Set to file:/path/to/file to write to a file. Set to null to discard.

User=appuser and Group=appgroup drop privileges: systemd starts the process as root, then switches to the named user and group before executing the command. Essential for security — never run services as root unless absolutely necessary.

WorkingDirectory=/path sets the process's working directory before starting.

Environment="VAR=value" sets environment variables. Use EnvironmentFile=/path/to/file to load variables from a file (one per line, KEY=value format).

TimeoutStartSec=30 and TimeoutStopSec=30 set how long systemd waits for the service to start or stop before killing it. Default is 90 seconds. Adjust for slow applications.

KillMode=process (default) sends SIGTERM to the main process only. Set to mixed to send SIGTERM to the main process and SIGKILL to child processes. Set to none if the service handles its own cleanup.

The Install Section

WantedBy=multi-user.target links this unit into the multi-user.target — the standard boot target. When you run systemctl enable myservice, systemd creates a symlink in /etc/systemd/system/multi-user.target.wants/ pointing to your unit file. On boot, systemd activates everything in that directory.

Other targets: graphical.target (desktop environment), rescue.target (single-user mode), socket.target (socket activation).

Common Patterns

A simple web service:

[Unit]
Description=My Web App
After=network.target postgresql.service

[Service]
Type=simple
User=webapp
WorkingDirectory=/opt/webapp
ExecStart=/opt/webapp/bin/server
Restart=on-failure
RestartSec=5
StandardOutput=journal

[Install]
WantedBy=multi-user.target

A background worker with environment file:

[Service]
Type=simple
User=worker
EnvironmentFile=/etc/worker/config.env
ExecStart=/usr/local/bin/worker
Restart=always
RestartSec=10

A one-time setup task:

[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-db
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Validation and Reload

After writing a unit file, validate it:

systemd-analyze verify /etc/systemd/system/myservice.service

Then reload the systemd daemon:

sudo systemctl daemon-reload

This tells systemd to re-read all unit files. Without this step, systemctl won't see your changes.

When This Breaks

The service starts but immediately exits. Check the exit code: systemctl status myservice. Check logs: journalctl -u myservice -n 50. Verify ExecStart is an absolute path and the binary exists. If the process forks, you may need Type=forking.

The service hangs on stop. Increase TimeoutStopSec. Verify ExecStop actually stops the process. If the process ignores SIGTERM, set KillMode=mixed to send SIGKILL to children.

Dependencies don't work. After= only orders units; it doesn't require them. Use Requires= for hard dependencies. Check dependency chains with systemctl list-dependencies myservice.

Privilege dropping fails. Verify the user exists: id appuser. Verify the binary is readable by that user. Some binaries need to run as root initially — use ExecStartPre to set up resources as root, then switch users in ExecStart.

Trade-offs

Systemd unit files are declarative and standardized, which makes them portable and auditable. The trade-off is rigidity: you can't use shell logic or conditional branching. For complex startup sequences, layer ExecStartPre commands or use a wrapper script.

Restart logic is simple but not sophisticated. For advanced retry strategies, use a separate service supervisor or orchestration tool. If you're running application services in a development environment, monitoring aplikasi development dengan Prometheus is worth reading for a complementary observability layer.

For services that depend on a web stack, the hosting environment also matters — the comparison on wpcompass.io covers how dedicated vs shared hosting affects service reliability in practice.

One-Line Takeaway

A systemd service unit file is your contract with the init system: declare what to run, how to run it, when to run it, and systemd handles the rest.