Systemd Service Unit File Tutorial

by Liam Foster
Systemd Service Unit File Tutorial

A systemd service unit file is a declarative config that tells your OS how to start, stop, and manage a long-running process. Instead of init scripts, you write a .service file in /etc/systemd/system/ and systemd handles the rest—restarts on crash, dependency ordering, logging. This is the modern Linux standard.

The Unit File Structure

Every systemd service unit file has three sections: [Unit], [Service], and [Install]. Think of it as metadata, execution rules, and boot behavior.

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

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

[Install]
WantedBy=multi-user.target

The [Unit] section describes what this service is and what it depends on. [Service] defines how to run the process. [Install] tells systemd when to enable this service at boot.

Core Directives You Need

Description: A human-readable string. Shows up in systemctl status and logs. Keep it short and specific.

After/Before: Ordering directives. After=network.target means "start this service after the network is ready." Do not confuse this with dependency—use Requires= or Wants= if you need hard dependencies.

Type: Declares how the process behaves.

  • Type=simple (default): systemd assumes the process runs in the foreground. Use this for most apps.
  • Type=forking: The process forks into the background and the parent exits. Older daemons do this. Systemd waits for the parent to exit, then considers the service started.
  • Type=oneshot: The process runs once and exits. Useful for setup scripts, not long-running services.
  • Type=notify: The process sends a notification to systemd when it's ready. Requires the app to use the systemd notification protocol.

ExecStart: The command to run. Use absolute paths. If the command has arguments, quote them or use an array syntax:

ExecStart=/usr/bin/myapp --config=/etc/myapp.conf

User/Group: The OS user and group that runs the process. Never run as root unless you have no choice. Create a dedicated user: useradd -r -s /bin/false appuser.

Restart: When to restart the service if it exits.

  • Restart=no (default): Don't restart.
  • Restart=on-failure: Restart only if the exit code is non-zero or a signal kills it.
  • Restart=always: Restart no matter what.
  • Restart=on-abnormal: Restart on non-zero exit or signal, but not on clean exit.

Pair this with RestartSec=5 to add a delay between restarts (prevents restart loops).

WantedBy: The target that "wants" this service. multi-user.target is the standard boot target. This enables the service to start at boot when you run systemctl enable.

Writing Your First Unit File

Create /etc/systemd/system/myapp.service:

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

[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --port=8080
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Then reload and enable:

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

Check status:

sudo systemctl status myapp.service
journalctl -u myapp.service -f

Environment Variables and Config

Pass environment variables in the unit file:

[Service]
Environment="NODE_ENV=production"
EnvironmentFile=/etc/myapp/env

Or reference a file with EnvironmentFile=/etc/myapp/env, which should contain KEY=value pairs (one per line). This is cleaner than hardcoding secrets in the unit file.

Gotchas and Common Mistakes

After ≠ Requires: After=postgresql.service means "start after postgres," not "fail if postgres is down." Use Wants=postgresql.service for a soft dependency (systemd tries to start it but doesn't fail if it's unavailable) or Requires=postgresql.service for a hard dependency (systemd fails this service if the dependency fails).

Restart loops: If you set Restart=always without a RestartSec delay, a crashing app will hammer your CPU. Always add RestartSec=5 or higher.

Foreground vs. background: If your app daemonizes itself (forks to background), use Type=forking and set PIDFile=/var/run/myapp.pid. If it stays in the foreground, use Type=simple. Mixing them up causes systemd to think the service failed.

File permissions: The unit file must be readable by root. Use chmod 644 /etc/systemd/system/myapp.service.

Reload after edits: Always run sudo systemctl daemon-reload after changing a unit file. Systemd caches the config.

StandardOutput/StandardError: By default, output goes to /dev/null. Set both to journal to capture logs in journald, then read them with journalctl -u myapp.service. This is essential for debugging. If you want a fuller observability stack alongside your services, the self-hosted Prometheus and Grafana setup on devbox.id is worth a look.

Trade-Offs and When This Breaks

Systemd unit files are declarative, which means less control over startup order than shell scripts. If you need custom logic ("run this script, check its output, then decide what to do"), you'll need a wrapper script or multiple unit files with dependencies.

Restart policies are simple: you can't say "restart on failure, but not if the exit code is 42." For complex retry logic, write a wrapper or use a process supervisor like supervisord.

If your application doesn't handle signals cleanly, systemctl stop may hang. Set TimeoutStopSec=10 to force-kill after 10 seconds.

Checklist for Production

  • Use Type=simple unless the app forks itself.
  • Set User to a non-root account.
  • Add Restart=on-failure and RestartSec=10.
  • Capture output: StandardOutput=journal and StandardError=journal.
  • Use After=network.target if the app needs the network.
  • Use Wants= or Requires= for soft or hard dependencies, not just After=.
  • Test with systemctl start, systemctl stop, and check logs with journalctl -u myapp.service -f.
  • Verify the service restarts on crash: kill -9 $(pgrep -f myapp) and watch it come back.
  • Run systemctl daemon-reload after any edit.

One-Line Takeaway

Systemd unit files replace shell init scripts: declare what your app is, how to run it, and when to start it, and systemd handles the rest—restarts, logging, dependency ordering, and boot integration.