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 long-running process on Linux. Instead of writing shell scripts and init.d wrappers, you declare the service behavior in a declarative format—systemd handles the rest.

Unit files live in /etc/systemd/system/ (custom services) or /usr/lib/systemd/system/ (packaged services). The filename matters: myapp.service tells systemd this is a service unit.

The Three Essential Sections

Every service unit file has three main blocks: [Unit], [Service], and [Install].

[Unit] describes the service metadata and dependencies. Description= is human-readable. After= and Before= control ordering—if your app needs the network, set After=network-online.target. Wants= and Requires= express soft and hard dependencies on other units.

[Service] defines how the process runs. Type= specifies the startup behavior: simple (default, process runs in foreground), forking (process daemonizes itself), or notify (process signals systemd when ready). ExecStart= is the command to run. User= and Group= set the process owner—always use a dedicated user, never root. Restart= controls restart policy: on-failure restarts only on non-zero exit, always restarts regardless.

[Install] controls where the unit activates in the boot sequence. WantedBy=multi-user.target means "start this at boot in multi-user mode." RequiredBy= creates a hard dependency (rarely needed for services).

A Minimal Working Example

Here's a service for a Node.js app:

[Unit]
Description=My Node App
After=network.target

[Service]
Type=simple
User=nodeapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Breakdown: systemd waits for the network, then starts /usr/bin/node server.js as user nodeapp from /opt/myapp. If the process exits with an error, systemd waits 5 seconds and restarts it. At boot, this unit is pulled in by multi-user.target.

Critical Directives and Gotchas

ExecStart must be an absolute path. Systemd does not search $PATH. Use /usr/bin/node, not node. If your script is in a nonstandard location, write the full path or symlink it to /usr/local/bin/.

Type=forking is a trap. If your application forks itself into the background, set Type=forking and provide a PIDFile=/path/to/pidfile so systemd knows which process to track. Many modern apps don't fork; they run in the foreground and let systemd handle backgrounding. Prefer Type=simple unless the app explicitly documents that it forks.

StandardOutput and StandardError control logging. By default, output goes to the journal (systemd's log). To redirect to a file, use StandardOutput=file:/var/log/myapp.log and StandardError=file:/var/log/myapp.err. The journal is preferred—query it with journalctl -u myapp.service.

Environment variables require explicit declaration. Set them with Environment=KEY=value or load from a file with EnvironmentFile=/etc/myapp/config. Systemd does not inherit the shell environment.

WorkingDirectory must exist. If the directory doesn't exist, the service fails silently. Create it beforehand or use ExecStartPre= to ensure it exists.

Dependency and Ordering Patterns

After= and Before= control order, not dependency. If service A has After=service-b.service, systemd starts B first, but doesn't fail A if B fails. Use Wants= for soft dependencies (start B, but don't fail if it doesn't) or Requires= for hard dependencies (fail A if B fails).

Common targets:

  • network.target: basic networking
  • network-online.target: full network connectivity (slower, more reliable)
  • multi-user.target: all standard services running
  • graphical.target: X11/Wayland ready

If your app queries DNS, use After=network-online.target and add Wants=network-online.target to ensure the network is fully up before starting.

Restart Logic and Failure Handling

Restart=on-failure restarts only if the process exits with a nonzero status or is killed by a signal. RestartSec=5 waits 5 seconds between restarts. For production services, combine with StartLimitInterval= and StartLimitBurst= to prevent restart loops:

Restart=on-failure
RestartSec=5
StartLimitInterval=60
StartLimitBurst=3

This allows 3 restarts within a 60-second window; after that, systemd gives up and marks the service as failed. Prevents a broken app from consuming CPU indefinitely.

Resource Limits

Set process limits to prevent runaway resource consumption:

LimitNOFILE=65536
LimitNPROC=4096
CPUQuota=50%
MemoryLimit=512M

LimitNOFILE caps open file descriptors. LimitNPROC caps child processes. CPUQuota throttles CPU (50% = one core on a two-core system). MemoryLimit enforces a hard memory ceiling—the kernel kills the process if it exceeds it.

When This Breaks

Service won't start. Check journalctl -u myapp.service -n 50 for errors. Common causes: ExecStart path doesn't exist, User doesn't exist, WorkingDirectory missing, syntax error in the unit file.

Service starts but crashes immediately. The process may need environment variables, config files, or dependencies. Verify the command works manually as the declared User. Check stdout/stderr in the journal.

Restart loop. The service fails, restarts, fails again. Set StartLimitBurst=0 temporarily to disable the restart limit and investigate the root cause. Once fixed, restore the limit.

Unit file changes not applied. After editing, run systemctl daemon-reload to tell systemd to re-read the file. Without this, the old configuration persists.

Deployment Checklist

  1. Write the unit file in /etc/systemd/system/myapp.service.
  2. Verify syntax: systemd-analyze verify myapp.service.
  3. Run systemctl daemon-reload.
  4. Test: systemctl start myapp.service and check systemctl status myapp.service.
  5. Verify logging: journalctl -u myapp.service.
  6. Enable at boot: systemctl enable myapp.service.
  7. Reboot and confirm the service starts automatically.

If you want a clean, containerized alternative for local development, Docker untuk local development setup is worth a look before committing to systemd-managed services on your workstation.

One-Line Takeaway

A systemd service unit file replaces shell scripts and init.d wrappers with a declarative configuration that systemd uses to manage process lifecycle, logging, and restart behavior—write it once, systemd handles the rest.