A systemd service unit file is a text configuration that tells Linux how to start, stop, and manage a background process (daemon). It replaces the old init.d scripts with declarative syntax: you describe the desired state, systemd enforces it.
Unit files live in /etc/systemd/system/ (custom services) or /usr/lib/systemd/system/ (package-provided). Systemd reads them at boot and on-demand, parsing sections like [Unit], [Service], and [Install].
Core Structure
Every service unit file has three main sections:
[Unit] — metadata and dependencies
Description: Human-readable name (shows in logs andsystemctl status).After: Ordering hint.After=network-online.targetmeans "start after network is ready," not "wait for it." UseRequires=orWants=if you need hard dependency.Requires: Service fails if listed unit fails. Rarely needed;Wants=is safer.Wants=: Service continues even if listed unit fails. Preferred for optional deps.
[Service] — the process itself
Type: How systemd tracks the process.simple(default) means the main process runs in foreground.forkingmeans it daemonizes (forks to background).notifymeans the process signals systemd when ready.ExecStart: The command to run. Must be an absolute path; no shell expansion.ExecStop: Optional command to gracefully stop the service.UserandGroup: POSIX user/group to run as. Systemd drops privileges; never run as root unless necessary.Restart: Restart policy.on-failurerestarts only on non-zero exit.alwaysrestarts even on clean exit (useful for long-running daemons).no(default) never restarts.RestartSec: Seconds to wait before restarting. Default 100ms.
[Install] — boot-time behavior
WantedBy=: Which target "wants" this service.multi-user.targetis the standard boot target. Creates a symlink in/etc/systemd/system/multi-user.target.wants/when you runsystemctl enable.
Minimal Example
[Unit]
Description=My Python App
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/main.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
Save this as /etc/systemd/system/myapp.service. Run systemctl daemon-reload (tells systemd to re-parse unit files), then systemctl enable myapp.service (adds boot symlink), then systemctl start myapp.service.
Critical Directives and Gotchas
ExecStart must be absolute, no pipes or redirects. If your app needs shell syntax, wrap it:
ExecStart=/bin/bash -c 'exec /usr/bin/myapp > /var/log/myapp.log 2>&1'
The exec replaces the bash process, so systemd tracks the right PID.
Type=forking is a trap. Old daemons fork themselves to background. Systemd loses track of the real process unless you also set PIDFile=/run/myapp.pid. Avoid it; rewrite the app to run in foreground (Type=simple) or use Type=notify with systemd library bindings. If you must fork, test with systemctl status — it should show the main PID, not a dead parent.
StandardOutput and StandardError default to journal. Logs go to systemd's journal, visible via journalctl -u myapp.service. This is intentional; don't redirect to files unless you have a reason (log rotation, archival, etc.). If you do, set up logrotate or use journal and query with journalctl.
Restart=always will restart even on clean shutdown. If your app exits 0 (success), always still restarts it. Use on-failure unless you want a watchdog loop. Combine with RestartSec=10 to avoid hammering.
PrivateTmp=yes isolates /tmp. Each service gets its own /tmp mount. Useful for security; breaks apps that expect shared temp. Check app docs.
Environment variables are not inherited from your shell. Set them in the unit file:
Environment="LOG_LEVEL=debug"
EnvironmentFile=/etc/myapp/env
EnvironmentFile reads key=value pairs (one per line, no quotes needed).
Dependency Model
Systemd uses a graph of dependencies. After= is an ordering constraint, not a hard requirement. If you need the service to fail if a dependency fails, use Requires=. If you want it to start after a target but tolerate failure, use Wants=.
Example: A web app that needs the database.
[Unit]
Description=Web App
After=postgresql.service
Requires=postgresql.service
If PostgreSQL stops, systemd stops the web app. If PostgreSQL never starts, the web app never starts.
For optional deps (nice-to-have):
After=redis.service
Wants=redis.service
The app starts regardless, but systemd orders it after Redis if Redis is enabled.
Testing and Debugging
- Syntax check:
systemd-analyze verify /etc/systemd/system/myapp.service - Dry-run:
systemctl start --no-block myapp.servicestarts asynchronously; check status withsystemctl status myapp.service. - Logs:
journalctl -u myapp.service -n 50shows last 50 lines. Add-fto follow in real-time. - Reload after edits:
systemctl daemon-reloadre-parses all unit files. - Check what's enabled:
systemctl list-unit-files --state=enabledshows boot-time services.
When This Breaks
Service won't start: Check systemctl status myapp.service for the error. Common causes: wrong path in ExecStart, missing user/group, permission denied, syntax error in unit file. Use systemd-analyze verify first.
Service restarts in a loop: Restart=always with a crashing app creates a restart loop. Check logs with journalctl -u myapp.service --no-pager | tail -20. Fix the app, then restart the service.
Dependencies not working: After= doesn't guarantee the dep is running; it only orders the start. Use Requires= or Wants= if you need the dep to actually be up.
Permission denied on log files: If User=appuser but logs are in /var/log/ (owned by root), the app can't write. Either create the log dir with correct ownership (mkdir -p /var/log/myapp && chown appuser:appgroup /var/log/myapp) or use the journal.
Trade-offs
Systemd unit files are declarative and portable across distributions. The cost: less flexibility than shell scripts. If your startup logic is complex (conditional checks, loops), consider a wrapper script and call it from ExecStart.
Using the journal (StandardOutput=journal) centralizes logs and integrates with systemd tools, but requires learning journalctl. File-based logs are simpler for traditional log aggregation (Splunk, ELK) but need rotation and cleanup.
Privacy and security directives (PrivateTmp=, NoNewPrivileges=, ProtectSystem=) improve isolation but may break legacy apps. For hardening ideas beyond the unit file itself — such as locking down user accounts running these services — the writeup on digitalwarga.id covers two-factor authentication as an additional layer worth considering. Test in a non-prod environment first.
One-line takeaway
Write a systemd service unit file to declare what your daemon is, how to run it, and when to restart it—then let systemd enforce it reliably across reboots and failures.