A systemd service unit file is a configuration that tells your Linux system how to start, stop, and manage a long-running process. Instead of manual shell scripts or init.d files, you write a declarative unit file and systemd handles the lifecycle—restart on crash, dependency ordering, resource limits, logging.
This tutorial covers the anatomy of a service file, the directives that matter, and the edge cases that trip up first attempts.
The Basic Structure
A systemd service unit file lives in /etc/systemd/system/ (or /usr/lib/systemd/system/ for packaged services) with a .service extension. It has three main sections: [Unit], [Service], and [Install].
Here's a minimal example:
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure
[Install]
WantedBy=multi-user.target
The [Unit] section declares what the service is and what it depends on. The [Service] section defines how to run it. The [Install] section tells systemd where this service should be enabled.
Unit Section: Metadata and Dependencies
Description is human-readable text that appears in logs and systemctl status. Keep it under 80 characters.
After=network.target means: do not start this service until the network is available. This is critical for services that need DNS or external connectivity. Use Before= if your service must start before something else (rare).
Wants= and Requires= express soft and hard dependencies. Wants= means "if the target exists, start it first, but don't fail if it doesn't." Requires= means "fail this service if the dependency fails." For most cases, After= alone is enough.
ConditionPathExists=/etc/myapp/config will skip starting the service if the file doesn't exist—useful for optional configurations without failing the boot.
Service Section: The Runtime Contract
Type=simple (the default) means the process stays in the foreground. systemd considers the service started once ExecStart returns. This is what you want for most applications.
Type=forking is for legacy daemons that fork and exit the parent. Avoid this unless you're wrapping an old service. systemd will wait for the parent to exit, then assume the child is the service. It's fragile.
Type=notify means your application sends a readiness signal to systemd via sd_notify(). Only use this if your app supports it (common in Go, Rust, systemd-aware frameworks).
ExecStart=/path/to/binary arg1 arg2 is the command that runs the service. Absolute paths only. If you need shell features (pipes, redirects), wrap it: ExecStart=/bin/bash -c 'command | other'—but this adds a process layer and complicates signal handling.
ExecStartPre= and ExecStartPost= run before and after ExecStart. Use them for validation or cleanup, not as a substitute for proper application startup.
User=appuser and Group=appgroup drop privileges. Create the user with useradd -r -s /bin/false appuser (system user, no shell). This is not optional in production.
WorkingDirectory=/var/lib/myapp sets the working directory. Relative paths in your app will resolve from here.
Restart=on-failure restarts the service if it exits with a non-zero status or is killed by a signal. Restart=always restarts regardless of exit code (use for services that must stay up). Restart=no disables auto-restart.
RestartSec=5s waits 5 seconds between restart attempts. Prevents restart loops from hammering your system.
StandardOutput=journal and StandardError=journal send stdout and stderr to the systemd journal (visible via journalctl). This is the default and the right choice. Avoid redirecting to files unless you have a specific reason.
Environment=VAR=value sets environment variables. Prefer EnvironmentFile=/etc/myapp/env to load from a file—cleaner and reloadable without editing the unit file.
LimitNOFILE=65536 sets resource limits (open file descriptors, memory, CPU). Match your application's needs; the system defaults are often too low for high-traffic services.
Install Section: Boot and Enablement
WantedBy=multi-user.target means this service is wanted by the multi-user target (normal system boot). When you run systemctl enable myapp.service, systemd creates a symlink in /etc/systemd/system/multi-user.target.wants/ pointing to your unit file.
WantedBy=graphical.target for desktop services. Don't mix them—pick one.
Common Patterns
Foreground application (Node, Python, Go):
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/app/server.js
Restart=on-failure
RestartSec=10s
User=appuser
StandardOutput=journal
StandardError=journal
Application with config reload:
[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/app.conf
ExecReload=/bin/kill -HUP $MAINPID
This allows systemctl reload myapp.service to send SIGHUP without restarting.
Socket activation (advanced):
Create a companion .socket unit that listens on a port or Unix socket. systemd starts the service only when traffic arrives. This is powerful for reducing idle resource use but requires application support.
When This Breaks
Service starts but immediately exits. Check the exit code: systemctl status myapp.service shows the last few lines of output. Run journalctl -u myapp.service -n 50 to see the full log. The application may be failing silently; verify it runs manually with the same User, WorkingDirectory, and Environment.
Service starts but systemd thinks it failed. If your app daemonizes itself (forks), use Type=forking and set PIDFile=/path/to/pid. Better: rewrite the app to not fork and use Type=simple.
Restart loop consuming CPU. Add RestartSec=30s and investigate why the service keeps crashing. Don't mask the problem with longer delays.
Permissions denied on log files. The User directive changes the effective user; ensure the working directory and log paths are writable by that user. Use umask or explicit chmod in ExecStartPre if needed.
Environment variables not set. Environment= in the unit file is not inherited from your shell. Use EnvironmentFile= to load from a file, or pass them explicitly. Verify with systemctl show -p Environment myapp.service.
Service hangs on stop. Set TimeoutStopSec=10s to force a kill after 10 seconds. If the service ignores SIGTERM, increase the timeout or rewrite the app to handle signals cleanly.
Workflow
- Write the unit file in
/etc/systemd/system/myapp.service. - Run
systemctl daemon-reloadto parse new files. - Test with
systemctl start myapp.service. - Check status:
systemctl status myapp.serviceandjournalctl -u myapp.service. - Enable on boot:
systemctl enable myapp.service. - Reboot and verify:
systemctl status myapp.service.
Key Takeaway
A systemd service unit file replaces shell scripts with a declarative, testable contract. Get Type, ExecStart, User, and Restart right, and most services run reliably. Test before enabling on boot.