A systemd service unit file is a declarative configuration that tells systemd how to start, stop, and manage a background process. It replaces the old init.d script model with structured key-value directives. Understanding the three core sections—Unit, Service, Install—is the foundation for reliable service management on Linux.
The Three-Section Model
Every service unit file follows this structure:
[Unit]
[Service]
[Install]
Think of it as: metadata, execution rules, and boot behavior. The Unit section defines what the service is and its dependencies. Service specifies how to run it. Install describes where systemd should symlink the unit file at boot time.
Writing the [Unit] Section
The Unit section answers: what is this service, and what must run before it?
Description is the human label:
[Unit]
Description=My Application Server
After and Before control ordering. Use After when your service depends on something else being ready first:
After=network-online.target
After=postgresql.service
After does not start those services; it only orders them. If you need them to start automatically, use Requires or Wants:
Wants=postgresql.service
Wants means "start postgresql if possible, but don't fail if it's missing." Requires means "fail this service if the dependency fails." Choose Wants for optional dependencies, Requires for critical ones.
ConditionPathExists gates startup on a file check:
ConditionPathExists=/etc/myapp/config.yml
The service won't start if that path doesn't exist. Useful for optional features or mounted volumes.
Writing the [Service] Section
The Service section is where execution happens. This is the most complex part.
Type defines how systemd knows the service started successfully. Four common values:
simple(default): the process stays in foreground. systemd considers it started when the process launches.forking: the process daemonizes (forks to background). systemd waits for the child PID.oneshot: the process runs once and exits. Useful for initialization tasks.notify: the process sends a signal to systemd when ready. Requires sd_notify() in your code.
Most applications use simple. Use forking only if the app daemonizes itself and you can't change that behavior.
ExecStart is the command to run:
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Use absolute paths. Environment variables expand: $HOME, $USER, custom ones you define in EnvironmentFile or Environment.
ExecStartPre and ExecStartPost run before and after ExecStart:
ExecStartPre=/usr/bin/mkdir -p /var/run/myapp
ExecStart=/usr/local/bin/myapp
ExecStartPost=/usr/bin/chown myapp:myapp /var/run/myapp
If ExecStartPre fails, ExecStart does not run.
User and Group set the privilege context:
User=myapp
Group=myapp
Systemd runs the process as that user. Create the user beforehand or systemd will fail silently.
WorkingDirectory sets the starting directory:
WorkingDirectory=/opt/myapp
Restart controls automatic restarts on failure:
Restart=on-failure
RestartSec=5
Options: no (never), always (every time it exits), on-failure (only if exit code is non-zero or signal), on-abnormal (signal or timeout). RestartSec is the delay in seconds before attempting restart.
StandardOutput and StandardError route logs:
StandardOutput=journal
StandardError=journal
Use journal to send to systemd's journal (readable via journalctl). Use file:/path/to/log to write directly to a file. Avoid syslog on modern systems.
TimeoutStartSec and TimeoutStopSec set deadlines:
TimeoutStartSec=30
TimeoutStopSec=10
If the service doesn't start within 30 seconds, systemd marks it failed. If it doesn't stop within 10 seconds, systemd kills it. Use appropriate values for your application; slow startups need longer timeouts.
Writing the [Install] Section
The Install section controls how systemctl enable behaves.
WantedBy is the most common directive:
[Install]
WantedBy=multi-user.target
When you run systemctl enable myapp.service, systemd creates a symlink in /etc/systemd/system/multi-user.target.wants/. At boot, multi-user.target pulls in your service.
Use multi-user.target for background services. Use graphical.target if the service is for desktop environments.
RequiredBy is stronger; the target fails if this service fails. Use rarely.
Alias creates alternative names:
Alias=myapp.service myapp-prod.service
Then systemctl start myapp-prod works even though the file is named myapp.service.
A Complete Example
[Unit]
Description=My Python Application
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
Environment="LOG_LEVEL=info"
ExecStart=/usr/bin/python3 /opt/myapp/main.py
StandardOutput=journal
StandardError=journal
Restart=on-failure
RestartSec=10
TimeoutStartSec=30
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target
Place this in /etc/systemd/system/myapp.service, then:
sudo systemctl daemon-reload
sudo systemctl enable myapp.service
sudo systemctl start myapp.service
When This Breaks
Service won't start, no error. Check journalctl -u myapp -n 50. Common causes: User doesn't exist, ExecStart path is wrong, WorkingDirectory doesn't exist, ConditionPathExists failed.
Service starts but crashes immediately. Logs will show the reason. Verify the process runs manually: sudo -u appuser /opt/myapp/main.py.
Changes don't take effect. Always run systemctl daemon-reload after editing a unit file. systemd caches parsed units.
Restart loop. If RestartSec is short and the process crashes fast, you'll see many restart attempts in logs. Increase RestartSec or fix the underlying issue.
Timeout during start. If your application takes longer to initialize than TimeoutStartSec, increase it. For applications that don't signal readiness, consider Type=forking with PIDFile instead of Type=simple.
Trade-Offs
Simple is safer than forking; forking requires the process to write its PID file correctly. Journal logging is convenient but adds overhead; file-based logging is faster but you own rotation. Restart=always is resilient but masks bugs; Restart=on-failure requires good exit codes but forces you to handle errors properly.
One-Line Takeaway
Systemd unit files replace shell scripts with declarative syntax: Unit defines dependencies, Service specifies execution, Install sets boot behavior—reload after editing, then enable and start.