Skip to main content

Systemd Deep Dive — Services, Timers, and Boot Targets

· 6 min read
Goel Academy
DevOps & Cloud Learning Hub

Your app crashes at 3 AM and nobody restarts it — systemd can fix that forever. Systemd is the init system and service manager on virtually every modern Linux distribution, and understanding it is non-negotiable for DevOps work.

Systemd Basics — systemctl

systemctl is your primary interface to systemd. Here are the commands you'll use daily.

# Start, stop, restart a service
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx

# Reload config without restarting (zero-downtime for supported services)
sudo systemctl reload nginx

# Enable service to start at boot / disable it
sudo systemctl enable nginx
sudo systemctl disable nginx

# Check service status (shows logs, PID, memory usage)
systemctl status nginx

The difference between enable and start trips up beginners:

CommandWhat It DoesWhen
startStarts the service right nowImmediately
enableCreates symlinks so it starts at bootNext boot
enable --nowEnables AND starts immediatelyBoth
# The shortcut — enable and start in one command
sudo systemctl enable --now nginx

Listing and Inspecting Services

# List all running services
systemctl list-units --type=service --state=running

# List all services (including inactive)
systemctl list-units --type=service --all

# List failed services — your first check after a reboot
systemctl --failed

# Show the full service unit file
systemctl cat nginx.service

# Show all properties of a service
systemctl show nginx.service

# Check if a specific service is active/enabled
systemctl is-active nginx
systemctl is-enabled nginx

Creating a Custom Service

This is where systemd becomes powerful. Let's say you have a Node.js API that needs to run as a daemon.

# Create a service unit file
sudo tee /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Node.js API Server
Documentation=https://github.com/myorg/myapp
After=network.target
Wants=network.target

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitBurst=5
StartLimitIntervalSec=60

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/myapp/data /var/log/myapp

# Environment
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=-/opt/myapp/.env

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target
EOF
# Reload systemd to pick up the new file
sudo systemctl daemon-reload

# Enable and start
sudo systemctl enable --now myapp.service

# Check it's running
systemctl status myapp.service

Key directives explained:

DirectiveValuePurpose
Restarton-failureRestart only on non-zero exit codes
RestartalwaysRestart no matter what (good for daemons)
RestartSec5Wait 5 seconds before restarting
StartLimitBurst5Max 5 restarts...
StartLimitIntervalSec60...within 60 seconds, then stop trying

Systemd Timers — The Modern Cron

Systemd timers are more powerful than cron: they support dependencies, randomized delays, persistent scheduling (runs missed jobs after boot), and proper logging through journald.

You need two files — a timer unit and a matching service unit.

# Create the service that does the actual work
sudo tee /etc/systemd/system/db-backup.service << 'EOF'
[Unit]
Description=Database Backup Script

[Service]
Type=oneshot
User=backup
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal
EOF
# Create the timer that triggers the service
sudo tee /etc/systemd/system/db-backup.timer << 'EOF'
[Unit]
Description=Run database backup daily at 2 AM

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=900
Persistent=true

[Install]
WantedBy=timers.target
EOF
# Enable and start the timer (not the service!)
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer

# List all active timers with next run time
systemctl list-timers --all

# Manually trigger the backup right now (for testing)
sudo systemctl start db-backup.service

# Check the last run result
systemctl status db-backup.service
journalctl -u db-backup.service --since today

Timer schedule examples:

OnCalendar ValueMeaning
*-*-* 02:00:00Every day at 2 AM
Mon *-*-* 09:00:00Every Monday at 9 AM
*-*-01 00:00:00First day of every month
*-*-* *:00/15:00Every 15 minutes
weeklyEvery Monday at midnight

Boot Targets

Targets are systemd's replacement for SysVinit runlevels. They group services together.

# See current default target
systemctl get-default

# Set default target (e.g., no GUI for servers)
sudo systemctl set-default multi-user.target

# Available targets
# graphical.target — Full GUI (runlevel 5)
# multi-user.target — Multi-user CLI (runlevel 3)
# rescue.target — Single user mode (runlevel 1)
# emergency.target — Minimal shell, no mounts

# Switch target without rebooting (careful!)
sudo systemctl isolate multi-user.target

Journalctl — Reading Systemd Logs

Every service managed by systemd logs to the journal. journalctl is how you read it.

# View logs for a specific service
journalctl -u nginx.service

# Follow logs in real-time (like tail -f)
journalctl -u myapp.service -f

# Logs since last boot
journalctl -b

# Logs from the previous boot (useful after crash)
journalctl -b -1

# Logs from the last hour
journalctl --since "1 hour ago"

# Logs between two timestamps
journalctl --since "2025-05-24 08:00" --until "2025-05-24 12:00"

# Show only errors and above
journalctl -u nginx.service -p err

# Output as JSON (pipe to jq for parsing)
journalctl -u nginx.service -o json-pretty --no-pager | head -50

Priority levels for the -p flag:

LevelNameUse
0emergSystem is unusable
1alertImmediate action required
2critCritical conditions
3errError conditions
4warningWarning conditions
5noticeNormal but significant
6infoInformational
7debugDebug-level messages

Service Dependencies and Ordering

Control the order services start and their relationships.

# View the dependency tree for a service
systemctl list-dependencies nginx.service

# In your unit file, use these directives:
# After= — Start after these units (ordering)
# Before= — Start before these units
# Requires= — Hard dependency (if dependency fails, this fails too)
# Wants= — Soft dependency (if dependency fails, continue anyway)
# BindsTo= — Strongest dependency (stops if dependency stops)

Example: a web app that needs the database running first.

sudo tee /etc/systemd/system/webapp.service << 'EOF'
[Unit]
Description=Web Application
After=network.target postgresql.service redis.service
Requires=postgresql.service
Wants=redis.service

[Service]
Type=simple
ExecStart=/opt/webapp/start.sh
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

This means: start after PostgreSQL and Redis, require PostgreSQL (fail if it's not running), want Redis (start without it if necessary).

Troubleshooting Failed Services

When things go wrong, here's your debugging checklist.

# Step 1: Check status and recent logs
systemctl status myapp.service

# Step 2: Get the full log output
journalctl -u myapp.service --no-pager -n 100

# Step 3: Verify the unit file syntax
systemd-analyze verify /etc/systemd/system/myapp.service

# Step 4: Check for resource limits
systemctl show myapp.service | grep -i limit

# Step 5: Analyze boot time and slow services
systemd-analyze blame
systemd-analyze critical-chain myapp.service

# Step 6: If stuck in "activating", check the process
systemctl show myapp.service -p MainPID

Next in our Linux series: Linux Log Analysis — learn how to find problems in logs before they become outages, using journalctl, grep patterns, awk, and log rotation strategies.