Writing a Custom Service Unit

systemd in Depth

Chapter 3 · Writing a Custom Service Unit

systemd1-2 showed the shape of a unit file without explaining any field in depth. This chapter is that depth — and the real service written here is the same one systemd1-11's own capstone actually deploys.

[Unit] Section In Depth

Description= is shown directly in systemctl status output — worth writing something genuinely useful, not a placeholder. Documentation= is optional, a URL or man page reference. After=/Requires= are previewed here; systemd1-4 covers them in full.

[Service] Section — Type=

The field that determines how systemd knows whether the service actually started successfully:

  • Type=simple (the default) — the process started by ExecStart= is the main process; systemd considers the service started the moment it forks
  • Type=forking — for older-style daemons that fork into the background and exit the original process; systemd needs PIDFile= to know which PID to actually track
  • Type=oneshot — a command that runs once and exits, not a long-running daemon at all; commonly paired with RemainAfterExit=yes so systemctl status still reports "active" after it finishes — directly relevant to systemd1-7's own timer-paired service units
  • Type=notify — the service itself signals "I'm actually ready now" back to systemd; the most precise option, but requires the application to support it

ExecStart=, ExecStop=, ExecReload=

ExecStart= is required — the actual command that is the service. It must use an absolute path, a genuinely common beginner mistake: no shell PATH lookup happens here the way it would in an interactive terminal. ExecStop= is optional — for Type=simple, systemd can usually just send SIGTERM to stop the process cleanly; only write one when a service needs a genuinely custom shutdown command. ExecReload= runs on systemctl reload NAME — a real, common third action beyond just start/stop, typically telling a running process to re-read its config without a full restart.

Restart= — Automatic Recovery

Restart=on-failure RestartSec=5

Restart=on-failure (or always, or the default no) tells systemd to automatically restart the service if it exits unexpectedly. RestartSec= adds a delay before the restart attempt, avoiding a tight crash-loop hammering the system. A genuinely valuable capability with no clean System V init equivalent — one more concrete payoff of systemd1-1's own "why systemd replaced init" material.

[Install] Section — WantedBy=

WantedBy=multi-user.target is what systemctl enable actually acts on — the line determining which target's own .wants/ directory receives the new symlink. Different WantedBy= values matter for different kinds of services, previewed here and covered fully in systemd1-5's own targets chapter.

A Full Worked Example — Toward the Capstone

[Unit] Description=My Application Service After=network.target [Service] Type=simple ExecStart=/usr/local/bin/myapp Restart=on-failure RestartSec=5 User=myapp [Install] WantedBy=multi-user.target

Every field here is explained above — this is the actual shape systemd1-11's own capstone deploys for real. User=myapp is worth a light callback of its own: running a service as a dedicated, non-root user rather than root by default is a genuinely security-relevant habit, the same least-privilege instinct owasp1/bc1 already established elsewhere on this site.

Applying a New or Changed Unit File

sudo systemctl daemon-reload sudo systemctl enable --now myapp

systemctl daemon-reload is required after creating or editing any unit file — it tells systemd to re-read unit files from disk. Forgetting it is a real, common "why isn't my change taking effect" gotcha.

Type="Started" means
simpleThe ExecStart= process forked — immediate
forkingThe tracked PID (via PIDFile=) is running, after the original process exits
oneshotThe command ran and exited; RemainAfterExit=yes keeps it reported as active
notifyThe application itself explicitly signaled readiness
Verify a unit file before using it
systemd-analyze verify myapp.service checks a unit file for syntax errors before you actually try to start it — worth running on any hand-written unit, before daemon-reload and start.
Forgetting daemon-reload is a real, common mistake
systemd keeps using its own cached, in-memory copy of unit files until explicitly told to reload — an edit to a unit file on disk can appear to have no effect at all if systemctl daemon-reload is skipped, in the same shape (a different subsystem, same underlying gotcha) as grub1-3's own "forgot update-grub" mistake.

Hands-On Exercises

Exercise 1

Write a complete unit file for a service named "backup-agent" that runs /opt/backup/agent, restarts automatically on failure after a 10-second delay, and starts after the network is up.

📄 View solution
Exercise 2

Explain why a one-time database migration script should use Type=oneshot with RemainAfterExit=yes rather than Type=simple, referencing what "started" actually means for each.

📄 View solution
Exercise 3

Someone edits /etc/systemd/system/myapp.service to change its Restart= setting, then runs systemctl restart myapp, but the old behavior persists. Explain the most likely cause and the fix.

📄 View solution

Chapter 3 Quick Reference

  • Type=simple (default, immediate), forking (needs PIDFile=), oneshot (+ RemainAfterExit=yes), notify (app signals readiness)
  • ExecStart= requires an absolute path — no shell PATH lookup; ExecStop= optional; ExecReload= for config reloads without a full restart
  • Restart=on-failure + RestartSec= — automatic recovery, no System V init equivalent
  • WantedBy= in [Install] determines which target's .wants/ gets the enable symlink
  • User= runs a service as a dedicated non-root user — a real least-privilege habit
  • systemctl daemon-reload is required after any unit file change — forgetting it is a real, common gotcha
  • systemd-analyze verify catches syntax errors before you try to use a hand-written unit