More practical tools: Planning dates and schedules? Try DateKit calculators. Managing money goals? Open BudgetKit planners. Need deep-work planning? Try FocusKit Weekly Planner.

Systemd: The Complete Guide for 2026

February 12, 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.

⚙ Related guides: Use systemd timers as a modern cron alternative — see our Bash Scripting Guide for the scripts they run, and our Linux Commands Guide for the foundation. For incident response ownership when timer-based ACK deadlines are breached, see Merge Queue ACK Timeout Remediation Runbook. When breaches repeat and handoffs stall, enforce cutoffs with the Merge Queue Escalation Decision Cutoff Guide. If the decision window expires after cutoff, run default actions from the Merge Queue Cutoff Window Expiry Enforcement Guide. After reopen approval, track breach signals with the Merge Queue Post-Reopen Monitoring Window Guide.

Table of Contents

  1. What Is Systemd
  2. Core Concepts: Units, Targets, Dependencies
  3. Essential systemctl Commands
  4. Writing Service Unit Files
  5. Service Types Explained
  6. Environment Variables and Configuration
  7. Restart Policies and Failure Handling
  8. Systemd Timers: Replacing Cron
  9. Socket Activation
  10. journalctl: Log Management
  11. Targets and the Boot Process
  12. Security Hardening
  13. User Services
  14. Debugging Systemd Issues
  15. Best Practices
  16. 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:

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:

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):

⚙ Build it faster: Use our Systemd Timer Generator to convert cron schedules to 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

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

Related Resources

Linux Commands Guide
Essential commands every Linux sysadmin needs
Docker Complete Guide
Containerize applications and orchestrate deployments
Nginx Configuration Guide
Configure the web server systemd manages
Merge Queue merge_group Trigger Guide
Useful incident runbook when deploy rollback checks stay pending in GitHub merge queue.
Merge Queue Flaky Required Checks Guide
Stabilize intermittent required checks before they extend incident rollback windows.
Merge Queue Checks Timed Out or Cancelled
Operational runbook for rollback PR checks that exceed time budgets or get cancelled by queue churn.
Merge Queue Required Check Name Mismatch Guide
Repair required-check naming drift when rollback PR status stays expected but never arrives.
Merge Queue Stale Review Dismissal Guide
Approval policy runbook for rollback PRs that repeatedly lose approval after queue updates.
Merge Queue Emergency Bypass Governance
Use dual approval, expiry, and restoration ownership when emergency rollback bypass is unavoidable.
Merge Queue Deny Extension vs Restore Baseline
Use an audit-first checklist to deny low-evidence extensions and return to baseline protections.
Merge Queue Appeal Outcome Closure Template
Post-incident closure template to lock ownership and due dates after appeal decisions.
Merge Queue Threshold Breach Alert Routing Playbook
Severity-based routing playbook for threshold breaches and owner escalation handoffs.
Merge Queue Escalation Decision Cutoff Guide
Authority-transfer matrix for recurring ACK timeout breaches and forced decision gates.
Merge Queue Post-Reopen Monitoring Window Guide
Post-reopen observation framework for immediate re-freeze decisions on guardrail breach.
Bash Scripting Guide
Write scripts for systemd timers and services
Linux Cheat Sheet
Quick reference for Linux commands and systemd