systemd Service Unit File Tutorial

by Liam Foster
systemd Service Unit File Tutorial

A systemd service unit file is a text configuration that tells systemd how to start, stop, and manage a process. It replaces the old init.d scripts with a declarative format. Understanding the structure and directives saves you from debugging mysterious service failures at 3 AM.

The Unit File Structure

Every systemd service unit file follows three main sections: [Unit], [Service], and [Install]. The file lives in /etc/systemd/system/ (for custom services) or /usr/lib/systemd/system/ (for package-provided services), with a .service extension.

[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 what the service is and what it depends on. The [Service] section defines how systemd runs the process. The [Install] section determines when the service starts (usually at boot). Each section uses key-value pairs separated by =.

Core Service Directives

Type controls how systemd considers the service "started." The most common value is simple: systemd assumes the service is active once ExecStart runs. Use forking if your application daemonizes itself (forks and exits the parent). Use oneshot for scripts that run once and exit; pair it with RemainAfterExit=yes if you want the service to stay "active" after completion.

ExecStart is the command that runs the service. Use the absolute path. If your command has arguments, quote them: ExecStart=/usr/bin/myapp --config /etc/myapp.conf. For shell features (pipes, redirects), wrap the command in /bin/bash -c "...", but avoid this when possible—it adds a shell layer that complicates signal handling.

Restart determines if systemd restarts the service when it exits. on-failure restarts only if the exit code is non-zero. always restarts regardless. on-abnormal restarts on signals like SIGKILL. Pair Restart with RestartSec=5 to wait 5 seconds between restarts, avoiding restart loops.

User and Group run the service under a specific account. Always use a dedicated, unprivileged user for security. Create it with useradd -r -s /usr/sbin/nologin myapp.

WorkingDirectory sets the working directory before ExecStart runs. Relative paths in your config should be relative to this directory.

StandardOutput and StandardError control where logs go. Set both to journal to send output to systemd's journal (readable via journalctl -u myapp). The default is inherit, which usually means terminal or /dev/null depending on context.

Dependencies and Ordering

The After directive in [Unit] specifies ordering: the service starts after the listed targets or services are active. After=network.target means "start after networking is up." This does not create a dependency—if the network service fails, your service still starts. To create a hard dependency, use Requires or Wants. Wants is softer: if the dependency fails, your service still starts. Requires is hard: if the dependency fails, your service does not start.

[Unit]
Description=My App
After=postgresql.service
Wants=postgresql.service

This means: start after PostgreSQL is up, but if PostgreSQL fails, start anyway. For a database-backed app, you might prefer Requires instead.

Installation and Enablement

The [Install] section tells systemctl enable where to link the unit file. WantedBy=multi-user.target is standard for services that run on boot in multi-user mode. After writing the file, run:

sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service

daemon-reload tells systemd to re-read the unit files. enable creates a symlink in /etc/systemd/system/multi-user.target.wants/ so the service starts at boot. start runs it immediately.

Common Gotchas

Signal handling: systemd sends SIGTERM by default when stopping a service. If your app doesn't handle SIGTERM, it may not shut down cleanly. Set KillSignal=SIGINT or KillSignal=SIGKILL if needed, but prefer fixing your app to handle SIGTERM. Use TimeoutStopSec=30 to force SIGKILL after 30 seconds if the service doesn't stop.

Environment variables: Use Environment="KEY=value" to set variables for the service. For multiple variables, use multiple Environment lines. Do not source a shell script in ExecStart—systemd does not run a shell, so source won't work. Instead, use EnvironmentFile=/etc/myapp/env to load variables from a file.

Path expansion: systemd does not expand ~ or $HOME. Use absolute paths or %h for the user's home directory. For example, ExecStart=/usr/bin/myapp --config %h/.config/myapp.conf.

Permissions on log files: If your app writes logs to a file, ensure the User account can write to that directory. A common mistake is running a service as myapp but the log directory is owned by root with mode 755.

Circular dependencies: Wants and Requires can create loops if not careful. systemd detects and breaks them, but the behavior may surprise you. Test with systemctl list-dependencies myapp.service.

Debugging and Validation

Validate the unit file syntax before enabling it:

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

This catches typos and missing sections. To see what systemd will actually do, use:

sudo systemctl cat myapp.service

This shows the merged configuration (useful if you have drop-in overrides). To check the service status and recent logs:

sudo systemctl status myapp.service
sudo journalctl -u myapp.service -n 50

The -n 50 flag shows the last 50 lines. Use -f to follow logs in real time, like tail -f.

Trade-Offs and Edge Cases

Using Type=simple is easiest but requires your app to stay in the foreground. If your app daemonizes itself, use Type=forking and set PIDFile so systemd can track the correct process. However, forking is fragile; modern apps should run in the foreground and let systemd handle supervision.

Setting Restart=always keeps a failed service running, which is useful for resilience but can mask bugs. A service that crashes every 5 seconds is still "running" but not useful. Use Restart=on-failure and log aggressively to catch the root cause.

Automatic restarts can create cascading failures if your service depends on external systems. If your database is down, restarting your app every 5 seconds wastes CPU and fills logs. Consider adding a health check or backoff logic in your app instead. If you're running services on your own infrastructure, the perbandingan VPS lokal vs internasional can also affect latency and reliability characteristics worth factoring into your restart strategy.

One-Line Takeaway

A systemd service unit file declaratively specifies how to run, restart, and depend on a process—write it once, then let systemd handle supervision and logging for the lifetime of your system.