Systemd: The Complete Guide for 2026
Systemd is the init system and service manager that runs as PID 1 on nearly every modern Linux distribution. It manages every process on your system from boot to shutdown: starting services in the correct order, monitoring their health, restarting them on failure, collecting their logs, and controlling system state transitions. If you deploy anything on Linux — web servers, databases, background workers, scheduled tasks — you are using systemd whether you realize it or not.
This guide covers systemd from fundamentals to advanced production patterns. Every section includes practical unit file examples and commands you can use immediately. Whether you are deploying a Node.js application, hardening a production server, replacing cron jobs, or debugging boot failures, this is the reference you need.
Table of Contents
- What Is Systemd
- Core Concepts: Units, Targets, Dependencies
- Essential systemctl Commands
- Writing Service Unit Files
- Service Types Explained
- Environment Variables and Configuration
- Restart Policies and Failure Handling
- Systemd Timers: Replacing Cron
- Socket Activation
- journalctl: Log Management
- Targets and the Boot Process
- Security Hardening
- User Services
- Debugging Systemd Issues
- Best Practices
- Frequently Asked Questions
1. What Is Systemd
Systemd was created by Lennart Poettering and Kay Sievers, first released in 2010. It replaced SysVinit and Upstart as the default init system on Fedora (2011), RHEL/CentOS (7+), Debian (8+), Ubuntu (15.04+), Arch Linux, openSUSE, and virtually every mainstream distribution. It is the single most important piece of userspace software on a modern Linux system.
Systemd is not just an init system. It is a suite of tools that manages:
- Service lifecycle — starting, stopping, restarting, and monitoring daemons
- Boot process — parallelized startup with dependency resolution
- Logging — the journal (journald) captures all output with structured metadata
- Timers — scheduled tasks with more features than cron
- Socket activation — on-demand service startup when connections arrive
- Device management — udev integration for hardware events
- Network configuration — systemd-networkd and systemd-resolved
- Temporary files — systemd-tmpfiles for managing /tmp and /run
The architecture centers on PID 1 (the systemd process itself), which reads unit files that declare what to run, when to run it, and how it depends on other units. This declarative approach is fundamentally different from SysVinit's imperative shell scripts.
2. Core Concepts: Units, Targets, Dependencies
Everything systemd manages is a unit. Units are defined in configuration files and come in several types:
- .service — a process or daemon (nginx, postgresql, your app)
- .timer — a scheduled trigger (replaces cron)
- .socket — a network or IPC socket for activation
- .target — a group of units (like runlevels)
- .mount / .automount — filesystem mount points
- .path — filesystem path monitoring
- .slice — resource management group (cgroups)
- .device — kernel device exposed to systemd
Unit files live in three locations, in order of priority:
/etc/systemd/system/ # Admin-created and overrides (highest priority)
/run/systemd/system/ # Runtime units (transient)
/usr/lib/systemd/system/ # Package-installed units (lowest priority)
Dependencies control startup ordering. The key directives are:
# Ordering (when to start relative to other units)
After=network.target # Start after network is up
Before=httpd.service # Start before Apache
# Requirement strength
Requires=postgresql.service # Hard dependency: if postgres fails, this fails too
Wants=redis.service # Soft dependency: start redis, but don't fail if it's missing
BindsTo=docker.service # Like Requires, but also stops when docker stops
# Conflict
Conflicts=iptables.service # Cannot run at the same time
A target is a synchronization point that groups units. The most common targets map to traditional runlevels:
multi-user.target # Full system, no GUI (runlevel 3)
graphical.target # Full system with GUI (runlevel 5)
rescue.target # Single-user mode (runlevel 1)
emergency.target # Minimal shell, no services
reboot.target # System reboot
poweroff.target # System shutdown
3. Essential systemctl Commands
systemctl is the primary command for interacting with systemd. Here are the commands you will use daily:
# Service lifecycle
sudo systemctl start nginx # Start a service now
sudo systemctl stop nginx # Stop a service now
sudo systemctl restart nginx # Stop then start
sudo systemctl reload nginx # Reload config without restart (if supported)
sudo systemctl reload-or-restart nginx # Reload if possible, restart otherwise
# Boot persistence
sudo systemctl enable nginx # Start at boot
sudo systemctl disable nginx # Don't start at boot
sudo systemctl enable --now nginx # Enable AND start immediately
sudo systemctl disable --now nginx # Disable AND stop immediately
# Status and inspection
systemctl status nginx # Show status, recent logs, PID, memory
systemctl is-active nginx # Returns "active" or "inactive"
systemctl is-enabled nginx # Returns "enabled" or "disabled"
systemctl is-failed nginx # Returns "failed" or not
systemctl show nginx # Show all properties (machine-readable)
systemctl cat nginx # Print the unit file contents
# Listing units
systemctl list-units # All loaded and active units
systemctl list-units --failed # Only failed units
systemctl list-unit-files # All installed unit files with state
systemctl list-timers # All active timers with schedule info
# System-wide
sudo systemctl daemon-reload # Reload unit files after editing
sudo systemctl daemon-reexec # Re-execute the systemd manager
systemctl get-default # Show default boot target
sudo systemctl set-default multi-user.target # Set boot target
# Dependencies
systemctl list-dependencies nginx # Show dependency tree
systemctl list-dependencies --reverse nginx # Show who depends on nginx
The daemon-reload command is essential. After creating or modifying any unit file, you must run it before systemd will see the changes.
4. Writing Service Unit Files
A service unit file has three sections: [Unit], [Service], and [Install]. Here is a complete example for a Node.js application:
# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
Documentation=https://github.com/myorg/myapp
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
Environment=NODE_ENV=production
Environment=PORT=3000
# Resource limits
LimitNOFILE=65536
MemoryMax=512M
CPUQuota=80%
[Install]
WantedBy=multi-user.target
Breakdown of each section:
[Unit] describes the unit and its relationships. Description appears in logs and status output. After controls startup ordering. Wants expresses a soft dependency.
[Service] defines how the service runs. Type tells systemd how to track the process. User/Group set the runtime identity. ExecStart is the command to run (must be an absolute path). Restart controls automatic restart behavior.
[Install] defines what happens on systemctl enable. WantedBy=multi-user.target means the service starts during normal multi-user boot.
The same pattern works for any language. For Python with Gunicorn, use ExecStart=/opt/app/venv/bin/gunicorn --workers 4 --bind 0.0.0.0:8000 wsgi:app with EnvironmentFile=/opt/app/.env for configuration.
5. Service Types Explained
The Type= directive tells systemd how the service signals readiness:
simple (default) — systemd considers the service started as soon as ExecStart begins. Use this for processes that stay in the foreground:
Type=simple
ExecStart=/usr/bin/node /opt/app/server.js
forking — the process forks and the parent exits. Systemd waits for the parent to exit and tracks the child. Use PIDFile to help systemd find the main process:
Type=forking
PIDFile=/run/nginx.pid
ExecStart=/usr/sbin/nginx
oneshot — the process runs and exits. Systemd waits for it to finish before considering the unit active. Ideal for initialization scripts:
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/setup-iptables.sh
notify — the process sends a readiness notification via sd_notify(). This is the most precise method. Used by services that need time to initialize:
Type=notify
ExecStart=/usr/sbin/httpd -DFOREGROUND
NotifyAccess=main
idle — like simple, but systemd delays execution until all jobs are dispatched. Useful for console output ordering during boot.
6. Environment Variables and Configuration
There are several ways to pass environment variables to a service:
# Inline in the unit file (small number of variables)
[Service]
Environment=DATABASE_URL=postgres://localhost:5432/mydb
Environment=REDIS_URL=redis://localhost:6379
Environment="SECRET_KEY=value with spaces"
# From a file (recommended for secrets and many variables)
[Service]
EnvironmentFile=/etc/myapp/config.env
EnvironmentFile=-/etc/myapp/local.env # "-" means don't fail if missing
The environment file format is simple key=value pairs:
# /etc/myapp/config.env
DATABASE_URL=postgres://localhost:5432/mydb
REDIS_URL=redis://localhost:6379
SECRET_KEY=your-secret-key-here
LOG_LEVEL=info
WORKERS=4
You can also use drop-in overrides to modify a unit without editing the original file:
# Create override directory
sudo mkdir -p /etc/systemd/system/myapp.service.d/
# Create override file
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
Environment=LOG_LEVEL=debug
MemoryMax=1G
# Or use the built-in editor (creates the override for you)
sudo systemctl edit myapp
Drop-in overrides are the correct way to customize package-installed services. Never edit files in /usr/lib/systemd/system/ directly because package updates will overwrite your changes.
7. Restart Policies and Failure Handling
The Restart= directive controls when systemd restarts a failed service:
Restart=no # Never restart (default)
Restart=on-failure # Restart only on non-zero exit code, signal, or timeout
Restart=on-abnormal # Restart on signal, timeout, or watchdog
Restart=on-abort # Restart only on signal (crash)
Restart=always # Always restart regardless of exit reason
RestartSec=5 # Wait 5 seconds before restarting
StartLimitIntervalSec=300 # Rate-limit window (5 minutes)
StartLimitBurst=5 # Max restarts within the window
For production services, Restart=on-failure is usually the right choice. It restarts on crashes but not on clean shutdowns (exit code 0). Use Restart=always for critical services that must never be down.
You can specify which exit codes count as success:
[Service]
Restart=on-failure
RestartSec=3
SuccessExitStatus=143 # Treat SIGTERM (143) as clean shutdown
RestartPreventExitStatus=1 # Don't restart on exit code 1 (config error)
For rate limiting, if the service restarts more than StartLimitBurst times within StartLimitIntervalSec, systemd marks it as failed and stops trying. To recover, run systemctl reset-failed myapp then systemctl start myapp.
Watchdog support lets systemd detect hung services:
[Service]
WatchdogSec=30 # Service must ping systemd every 30 seconds
WatchdogSignal=SIGABRT # Send this signal if watchdog times out
8. Systemd Timers: Replacing Cron
Systemd timers are a modern replacement for cron jobs. They support calendar schedules, monotonic intervals, persistent scheduling (catch up on missed runs), randomized delays, and full dependency management.
A timer requires two files: a .timer unit and a matching .service unit:
# /etc/systemd/system/backup.service
[Unit]
Description=Daily database backup
[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-database.sh
StandardOutput=journal
# /etc/systemd/system/backup.timer
[Unit]
Description=Run database backup daily at 2 AM
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
Enable the timer (not the service):
OnCalendar and generate the matching .timer + .service unit files.
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers # Verify it appears
OnCalendar uses a flexible syntax:
OnCalendar=hourly # Every hour
OnCalendar=daily # Every day at midnight
OnCalendar=weekly # Every Monday at midnight
OnCalendar=monthly # First day of month at midnight
OnCalendar=*-*-* 06:00:00 # Every day at 6 AM
OnCalendar=Mon-Fri *-*-* 09:30:00 # Weekdays at 9:30 AM
OnCalendar=*-*-01 00:00:00 # First of every month
OnCalendar=*:0/15 # Every 15 minutes
Test your calendar expressions with systemd-analyze calendar:
$ systemd-analyze calendar "Mon-Fri *-*-* 09:30:00"
Original form: Mon-Fri *-*-* 09:30:00
Normalized form: Mon..Fri *-*-* 09:30:00
Next elapse: Mon 2026-02-16 09:30:00 UTC
For interval-based timers (not calendar-based):
[Timer]
OnBootSec=5min # 5 minutes after boot
OnUnitActiveSec=1h # 1 hour after the service last ran
OnStartupSec=30s # 30 seconds after systemd starts
9. Socket Activation
Socket activation lets systemd listen on a port and start the service only when a connection arrives. This reduces boot time, saves resources for rarely-used services, and allows zero-downtime restarts.
# /etc/systemd/system/myapp.socket
[Unit]
Description=My App Socket
[Socket]
ListenStream=8080
Accept=no
BindIPv6Only=both
[Install]
WantedBy=sockets.target
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
Requires=myapp.socket
After=myapp.socket
[Service]
Type=simple
User=myapp
ExecStart=/opt/myapp/server --fd 3
NonBlocking=true
[Install]
WantedBy=multi-user.target
Enable the socket (not the service directly):
sudo systemctl enable --now myapp.socket
When a connection arrives on port 8080, systemd starts myapp.service and passes the socket as file descriptor 3. The application must be written to accept sockets from systemd (using sd_listen_fds() in C, or the python-systemd library, or the SD_LISTEN_FDS_START environment variable).
10. journalctl: Log Management
The systemd journal replaces traditional syslog with a structured, indexed binary log. journalctl is the tool to query it:
# View all logs
journalctl
# Follow logs in real time (like tail -f)
journalctl -f
# Logs for a specific service
journalctl -u nginx
journalctl -u nginx -f # Follow nginx logs
# Time-based filtering
journalctl --since "2026-02-12 10:00"
journalctl --since "1 hour ago"
journalctl --since "yesterday" --until "today"
journalctl -u myapp --since "5 min ago"
# Priority filtering (0=emerg to 7=debug)
journalctl -p err # Errors and above
journalctl -p warning -u myapp # Warnings from myapp
# Boot-based filtering
journalctl -b # Current boot only
journalctl -b -1 # Previous boot
journalctl --list-boots # List all recorded boots
# Output formats
journalctl -u myapp -o json # JSON output
journalctl -u myapp -o json-pretty # Pretty JSON
journalctl -u myapp -o short-iso # ISO timestamps
journalctl -u myapp -o cat # Message only (no metadata)
# Useful options
journalctl -u myapp -n 50 # Last 50 lines
journalctl -u myapp --no-pager # Don't use pager
journalctl --disk-usage # Show journal disk usage
journalctl -k # Kernel messages only (dmesg)
journalctl _PID=1234 # Logs from specific PID
Configure journal retention in /etc/systemd/journald.conf with Storage=persistent, SystemMaxUse=500M, MaxRetentionSec=1month, and Compress=yes.
11. Targets and the Boot Process
The boot process follows a dependency chain from default.target (usually multi-user.target on servers) backward through its dependencies. Systemd parallelizes wherever possible:
# View the default target
systemctl get-default
# Change the default target
sudo systemctl set-default multi-user.target
# Boot process analysis
systemd-analyze # Total boot time
systemd-analyze blame # Time taken by each unit
systemd-analyze critical-chain # Critical path visualization
systemd-analyze plot > boot.svg # SVG timeline of entire boot
Creating a custom target to group your application services:
# /etc/systemd/system/mystack.target
[Unit]
Description=My Application Stack
Requires=myapp.service
Requires=myworker.service
Requires=myscheduler.service
After=myapp.service myworker.service myscheduler.service
[Install]
WantedBy=multi-user.target
Now you can manage the entire stack with one command:
sudo systemctl start mystack.target # Start everything
sudo systemctl stop mystack.target # Stop everything
systemctl status mystack.target # See overall status
Emergency and rescue modes are accessible at boot via kernel parameters or at runtime:
sudo systemctl rescue # Drop to single-user mode
sudo systemctl emergency # Minimal emergency shell
12. Security Hardening
Systemd provides powerful sandboxing directives. A hardened service unit looks like this:
[Service]
# Run as non-root
User=myapp
Group=myapp
# Filesystem restrictions
ProtectSystem=strict # Mount / read-only (except /dev, /proc, /sys)
ProtectHome=yes # Hide /home, /root, /run/user
PrivateTmp=yes # Isolated /tmp and /var/tmp
ReadWritePaths=/var/lib/myapp # Allow writes only here
# Privilege restrictions
NoNewPrivileges=yes # Prevent privilege escalation
PrivateDevices=yes # No access to physical devices
ProtectKernelTunables=yes # Read-only /proc and /sys tunables
ProtectKernelModules=yes # Cannot load kernel modules
ProtectControlGroups=yes # Read-only cgroup filesystem
# Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# System call filtering
SystemCallArchitectures=native
SystemCallFilter=@system-service
# Other hardening
LockPersonality=yes # Prevent changing execution domain
MemoryDenyWriteExecute=yes # Prevent W+X memory mappings
RestrictRealtime=yes # No realtime scheduling
RestrictSUIDSGID=yes # Prevent setuid/setgid files
CapabilityBoundingSet= # Drop all capabilities
Audit your service with systemd-analyze security myapp to get a score and specific recommendations. Start with the basics and progressively add more restrictions, testing after each change.
13. User Services
Non-root users can manage their own services without sudo. Unit files go in ~/.config/systemd/user/:
# ~/.config/systemd/user/dev-server.service
[Unit]
Description=Local Development Server
[Service]
Type=simple
WorkingDirectory=%h/projects/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
Environment=PORT=3000
[Install]
WantedBy=default.target
Manage user services with systemctl --user:
systemctl --user daemon-reload
systemctl --user enable --now dev-server
systemctl --user status dev-server
journalctl --user -u dev-server -f
By default, user services only run while the user is logged in. To keep them running after logout:
sudo loginctl enable-linger username
This is useful for running persistent background tasks like syncthing, custom notifications, or development services without needing root access. The %h specifier in the unit file expands to the user's home directory.
14. Debugging Systemd Issues
When a service fails, follow this systematic approach:
# Step 1: Check the status (shows exit code, signal, recent logs)
systemctl status myapp
# Step 2: Read full logs for the service
journalctl -u myapp -n 100 --no-pager
# Step 3: Verify the unit file syntax
systemd-analyze verify /etc/systemd/system/myapp.service
# Step 4: Check if dependencies are running
systemctl list-dependencies myapp
# Step 5: Test the ExecStart command manually
sudo -u myapp /opt/myapp/start.sh
# Step 6: Check file permissions
ls -la /opt/myapp/
namei -l /opt/myapp/server.js # Check every path component
# Step 7: Check for resource limits
systemctl show myapp | grep -i limit
systemctl show myapp | grep -i memory
Common error codes and their meanings:
203/EXEC - Cannot execute the binary (wrong path, not executable, bad shebang)
217/USER - Specified User= does not exist
200/CHDIR - WorkingDirectory= does not exist
226/NAMESPACE - Namespace setup failed (check ProtectSystem/PrivateTmp)
209/STDOUT - Failed to set up stdout (check StandardOutput)
210/STDERR - Failed to set up stderr (check StandardError)
If a service keeps failing and hitting the rate limit:
sudo systemctl reset-failed myapp # Clear the failed state
sudo systemctl start myapp # Try again
For boot problems, add systemd.log_level=debug to the kernel command line (in GRUB) to get verbose systemd output during boot.
15. Best Practices
- Always use absolute paths in ExecStart, ExecStop, and other Exec directives. Systemd does not use PATH to resolve binaries.
- Run daemon-reload after every edit. Forgetting this is the number one cause of "my changes aren't working."
- Use drop-in overrides instead of editing vendor unit files. Run
systemctl edit myserviceto create override files that survive package updates. - Set appropriate Restart policy. Use
Restart=on-failurefor most services. UseRestart=alwaysonly for critical services that must never be down. - Use EnvironmentFile for secrets rather than inline Environment directives. Set restrictive permissions (0600) on the env file.
- Add security hardening progressively. Start with ProtectSystem, ProtectHome, PrivateTmp, and NoNewPrivileges. Run
systemd-analyze securityand add more restrictions. - Use Type=notify when possible. It gives systemd the most accurate picture of service readiness. Many modern daemons support it (nginx, PostgreSQL, systemd-aware applications).
- Set resource limits. Use MemoryMax, CPUQuota, and LimitNOFILE to prevent runaway services from affecting the entire system.
- Prefer timers over cron. Systemd timers have better logging, dependency management, randomized delays, and persistent scheduling.
- Log to journal. Set
StandardOutput=journalandStandardError=journalso all output is captured with full metadata. Avoid logging to files when journal integration is available. - Use systemctl cat and systemctl show to inspect the effective configuration of any service, including all overrides and defaults.
- Document your units. Use Description and Documentation directives. Future you (and your team) will be grateful.
Frequently Asked Questions
What is the difference between systemctl enable and systemctl start?
systemctl start immediately starts a service right now, but it will not survive a reboot. systemctl enable creates symlinks so the service starts automatically at boot, but does not start it immediately. To both start a service now and ensure it starts on boot, use systemctl enable --now myservice. Similarly, systemctl disable --now will stop the service and remove it from boot.
How do I replace a cron job with a systemd timer?
Create two files: a .service unit that defines what to run, and a .timer unit that defines when to run it. The timer file uses OnCalendar for calendar-based schedules or OnBootSec/OnUnitActiveSec for interval-based schedules. Place both in /etc/systemd/system/, run systemctl daemon-reload, then systemctl enable --now mytask.timer. Timers offer advantages over cron: persistent timers that catch up on missed runs, randomized delays, dependency management, and centralized logging via journalctl.
How do I view logs for a specific systemd service?
Use journalctl -u servicename to see all logs for that service. Add -f to follow logs in real time. Use --since and --until for time ranges: journalctl -u nginx --since '1 hour ago'. Use -p err to filter by priority level. Use -b for current boot only. Use -o json for JSON output. The journal stores logs in a binary format with full metadata, making it far more powerful than plain text log files.
What systemd security options should I use for my services?
At minimum, add ProtectSystem=strict, ProtectHome=yes, PrivateTmp=yes, and NoNewPrivileges=yes to your [Service] section. For network services, use RestrictAddressFamilies=AF_INET AF_INET6 to limit socket types. Use systemd-analyze security myservice to get a security score and specific recommendations. Add restrictions progressively and test after each change.
Why does my systemd service fail with status 203/EXEC or 217/USER?
Status 203/EXEC means systemd could not execute the binary specified in ExecStart. Common causes: the path is wrong, the file is not executable (chmod +x), or the shebang line is missing in a script. Always use absolute paths in ExecStart. Status 217/USER means the User= specified in the unit file does not exist. Create the user with useradd --system --no-create-home myuser. Check systemctl status and journalctl -u myservice for full details.
Related Resources
- Linux Commands: The Complete Guide — foundational commands every sysadmin needs alongside systemd
- Docker: The Complete Guide — containerize services that systemd manages on the host
- Nginx Configuration: The Complete Guide — configure the web server that systemd starts and monitors
- GitHub Merge Queue merge_group Trigger Guide — incident rollback fix when CI checks never start in queue context
- GitHub Merge Queue Flaky Required Checks Guide — reduce rollback delays from intermittent required-check failures
- GitHub Merge Queue Required Checks Timed Out or Cancelled Guide — unblock rollback PRs when required checks timeout or cancel repeatedly
- GitHub Merge Queue Required Check Name Mismatch Guide — resolve waiting-for-status deadlocks caused by required-check name drift
- GitHub Merge Queue Stale Review Dismissal Guide — fix rollback PR approval loops when stale-review rules invalidate approvals after queue churn
- GitHub Merge Queue Emergency Bypass Governance Guide — controlled approval model for incident rollback when queue-safe path cannot meet recovery SLA
- GitHub Merge Queue Deny Extension vs Restore Baseline Guide — checklist for rejecting weak extension requests and restoring default protections with audit evidence
- GitHub Merge Queue Appeal Outcome Closure Follow-Up Template Guide — finalize appeal outcomes with explicit owners and 24h/7d/30d follow-up checkpoints
- GitHub Merge Queue Closure Threshold Breach Alert Routing Playbook — route threshold breaches with severity ownership, escalation SLAs, and explicit handoff evidence
- GitHub Merge Queue Escalation Decision Cutoff for Repeated ACK Breaches Guide — trigger hard decision gates after recurring ACK misses to prevent ownership drift
- GitHub Merge Queue Post-Reopen Monitoring Window Guide — run post-reopen guardrail windows with immediate re-freeze triggers when control metrics regress
- Bash Scripting: The Complete Guide — write the scripts that systemd timers and services execute