If you’re running services on Linux, you’re almost certainly using systemd. But there’s a gap between knowing systemctl start nginx and actually writing your own robust service units. Let’s close that gap.
The Anatomy of a Service Unit
A systemd service unit lives in /etc/systemd/system/ and has three main sections:
| |
Let’s break down what matters:
[Unit] defines dependencies and metadata:
After=ensures services start in orderWants=creates soft dependencies (service still starts if dependency fails)Requires=creates hard dependencies (use sparingly)
[Service] defines the actual service behavior:
Type=simpleis most common — the process stays in foregroundType=forkingfor traditional daemons that fork to backgroundType=oneshotfor scripts that run once and exit
[Install] defines how the service integrates with system targets (runlevels).
A Real-World Example: Node.js Application
Here’s a production-ready service for a Node.js app:
| |
Notable additions:
EnvironmentFile=- — The - prefix means “don’t fail if file doesn’t exist.” Perfect for optional local config.
StartLimitInterval/StartLimitBurst — Prevents rapid restart loops. After 3 failures in 60 seconds, the service stops trying.
Security hardening — ProtectSystem=strict makes the filesystem read-only except for explicitly allowed paths. This is defense in depth.
Resource limits — Prevent runaway processes from taking down the whole server.
The Service Lifecycle
| |
The key insight: daemon-reload doesn’t restart services — it just tells systemd to re-read unit files. You still need to restart the service to apply changes.
Debugging Tips
Service won’t start? Check the logs:
| |
Need more detail? Enable debug output temporarily:
| |
Check dependencies:
| |
Verify syntax:
| |
Using systemctl edit
Don’t edit unit files directly for customizations — use drop-in overrides:
| |
This creates a drop-in that persists across package updates. Much cleaner than editing the main unit file.
Common Patterns
Run a command before start
| |
Graceful shutdown
| |
Multiple instances
Use template units (myapp@.service):
| |
Then: systemctl start myapp@3000 and systemctl start myapp@3001
Socket activation
Let systemd handle the socket, start your service on-demand:
| |
Security Checklist
Every production service should consider:
-
User=andGroup=— never run as root -
NoNewPrivileges=true— prevent privilege escalation -
ProtectSystem=strict— read-only filesystem -
ProtectHome=true— no access to /home -
PrivateTmp=true— isolated /tmp -
CapabilityBoundingSet=— drop unnecessary capabilities
For services that need network binding below 1024:
| |
Wrapping Up
systemd service units are deceptively simple. The basics get you running in minutes, but the depth is there when you need it — security hardening, resource limits, complex dependencies, socket activation.
Start simple. Add complexity as needed. And always, always run systemctl daemon-reload after edits.
Computing Arts is where I explore the craft of building with code and infrastructure. Find me at computingarts.com.