0Pricing
DevOps Bootcamp · Lesson

Controlling systemd Services and Writing Unit Files

Drive services with systemctl and author custom unit and timer files for scripted daemons.

Controlling systemd Services and Writing Unit Files is a free DevOps Bootcamp lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DevOps Bootcamp learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

Introduction to systemd and systemctl

systemd is the init system and service manager used by most modern Linux distributions. It is responsible for starting, stopping, and managing system services, as well as handling boot sequences and system state.

The primary tool for interacting with systemd is systemctl. With it you can:

  • Start and stop services
  • Enable or disable services at boot
  • Inspect service status and logs
  • Reload configuration without restarting

All services managed by systemd are defined by unit files, which are declarative configuration files stored under /etc/systemd/system/ (system-wide) or ~/.config/systemd/user/ (per-user).

Essential systemctl Commands

These are the most commonly used systemctl commands for day-to-day service management. Each command operates on a unit name such as nginx.service or simply nginx.

  • systemctl start <unit> — start a service immediately
  • systemctl stop <unit> — stop a running service
  • systemctl restart <unit> — stop then start (full restart)
  • systemctl reload <unit> — send SIGHUP to reload config without stopping
  • systemctl enable <unit> — create symlinks so the unit starts at boot
  • systemctl disable <unit> — remove those symlinks
  • systemctl status <unit> — show state, PID, recent log lines
#!/usr/bin/env bash
# Quick reference: inspect and control an nginx service
# (Requires nginx to be installed; run as root or with sudo)

systemctl status nginx
systemctl start nginx
systemctl enable nginx
systemctl reload nginx
systemctl restart nginx
systemctl stop nginx
systemctl disable nginx

Checking Service Status in Detail

systemctl status gives a rich snapshot of a unit. Understanding its output is essential for diagnosing problems quickly.

  • Loaded: path to the unit file and whether it is enabled at boot
  • Active: current state — active (running), inactive (dead), failed
  • Main PID: the process identifier of the service's main process
  • CGroup: all child processes belonging to the service
  • Log lines: the most recent journal entries for this unit

You can also query just the active state with systemctl is-active and the enabled state with systemctl is-enabled, which are ideal for use in scripts.

#!/usr/bin/env bash
# Script-friendly service health check
SERVICE="sshd"

if systemctl is-active --quiet "$SERVICE"; then
    echo "$SERVICE is running."
else
    echo "$SERVICE is NOT running. Attempting restart..."
    systemctl restart "$SERVICE"
fi

if systemctl is-enabled --quiet "$SERVICE"; then
    echo "$SERVICE is enabled at boot."
else
    echo "WARNING: $SERVICE is not enabled at boot."
fi

Listing and Filtering Units

When managing many services, you need ways to list and filter units efficiently. systemctl list-units shows all currently loaded units, while list-unit-files shows all installed unit files and their enabled state.

Useful filtering options:

  • --type=service — show only service units
  • --state=failed — show only failed units
  • --state=active — show only active units
  • --all — include inactive units in the listing

Combining these filters with grep lets you build powerful inspection pipelines in maintenance scripts.

#!/usr/bin/env bash
# List all failed services and report them
echo "=== Failed Services ==="
systemctl list-units --type=service --state=failed --no-legend

echo ""
echo "=== Services Enabled at Boot ==="
systemctl list-unit-files --type=service --state=enabled --no-legend | awk '{print $1}'

Anatomy of a systemd Service Unit File

A service unit file is a plain text INI-style file composed of sections. Each section controls a different aspect of the unit's lifecycle.

  • [Unit] — metadata: description, ordering dependencies (After=, Requires=, Wants=)
  • [Service] — how to start/stop the process: ExecStart, ExecStop, Restart, User, WorkingDirectory
  • [Install] — when this unit should be enabled: WantedBy=multi-user.target means it activates during normal multi-user boot

After placing or editing a unit file under /etc/systemd/system/, you must run systemctl daemon-reload so systemd picks up the changes.

Writing Your First Service Unit File

Let's create a simple service unit file for a custom Python HTTP server script. The unit file tells systemd exactly how to manage this process as if it were any other system service.

Key [Service] directives used here:

  • Type=simple — systemd treats the ExecStart process as the main process
  • User=www-data — run under a non-root account for security
  • WorkingDirectory — set the process's working directory
  • Restart=on-failure — automatically restart if the process exits with a non-zero code
  • RestartSec=5 — wait 5 seconds between restart attempts
#!/usr/bin/env bash
# Create a custom service unit file for a simple HTTP server
# Run as root

cat > /etc/systemd/system/mywebserver.service << 'EOF'
[Unit]
Description=My Custom Python Web Server
After=network.target

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/mysite
ExecStart=/usr/bin/python3 -m http.server 8080
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl enable --now mywebserver.service
systemctl status mywebserver.service

Service Types and Process Lifecycle

The Type= directive in [Service] tells systemd how to track when a service has finished starting. Choosing the wrong type is a common source of ordering bugs.

  • Type=simple — default; systemd considers the service started as soon as ExecStart launches. No readiness signal needed.
  • Type=forking — the ExecStart process forks and exits; the real daemon runs as a child. Use PIDFile= so systemd can track it.
  • Type=notify — the process sends sd_notify(READY=1) when truly ready. Most reliable for complex daemons.
  • Type=oneshot — runs once and exits; subsequent units wait for completion. Ideal for setup scripts.
  • Type=idle — like simple but execution is delayed until all active jobs finish.
#!/usr/bin/env bash
# Example: oneshot service for a database migration script
# /etc/systemd/system/db-migrate.service

cat > /etc/systemd/system/db-migrate.service << 'EOF'
[Unit]
Description=Run Database Migrations
After=postgresql.service
Requires=postgresql.service

[Service]
Type=oneshot
User=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/scripts/migrate.sh
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
EOF

systemctl daemon-reload
systemctl start db-migrate.service

Environment Variables and Security Hardening

Service unit files support several directives for passing environment variables and applying security restrictions — both are important in production systems.

Environment directives:

  • Environment="KEY=value" — set a single variable inline
  • EnvironmentFile=/etc/myapp/env — load variables from a file (one KEY=value per line)

Common security hardening directives:

  • NoNewPrivileges=true — prevent the process from gaining additional privileges via setuid
  • ProtectSystem=strict — mount /usr, /boot, /etc read-only
  • PrivateTmp=true — give the service its own isolated /tmp
  • CapabilityBoundingSet= — drop all Linux capabilities
#!/usr/bin/env bash
# Hardened service unit with environment file

cat > /etc/systemd/system/secureapp.service << 'EOF'
[Unit]
Description=Secure Application Daemon
After=network.target

[Service]
Type=simple
User=secureapp
Group=secureapp
WorkingDirectory=/opt/secureapp
EnvironmentFile=/etc/secureapp/env
ExecStart=/opt/secureapp/bin/server
Restart=on-failure
RestartSec=10

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
CapabilityBoundingSet=
ReadWritePaths=/var/lib/secureapp /var/log/secureapp

[Install]
WantedBy=multi-user.target
EOF

# Create the environment file separately so secrets stay out of the unit file
install -m 600 -o secureapp /dev/null /etc/secureapp/env
echo 'APP_PORT=9000' >> /etc/secureapp/env
echo 'DB_URL=postgresql://localhost/mydb' >> /etc/secureapp/env

systemctl daemon-reload

Introduction to systemd Timer Units

A systemd timer is a unit that activates another unit (usually a .service) on a schedule. It replaces traditional cron jobs with a more integrated, loggable, and controllable mechanism.

Every timer unit (foo.timer) is paired with a service unit of the same base name (foo.service) that does the actual work. You enable and start the timer, not the service directly.

Timer types:

  • Realtime (calendar) timers — fire at wall-clock times using OnCalendar= expressions like daily, weekly, or Mon *-*-* 02:00:00
  • Monotonic timers — fire relative to a system event using OnBootSec=, OnActiveSec=, or OnUnitActiveSec=

Writing a Timer Unit File

Let's build a complete timer + service pair that runs a backup script every day at 02:00. Notice how the [Timer] section lives in its own .timer file while the work lives in the paired .service file.

Key [Timer] directives:

  • OnCalendar= — calendar expression for the schedule
  • Persistent=true — if the system was off when the timer should have fired, run immediately on next boot
  • RandomizedDelaySec= — add up to N seconds of random jitter to prevent thundering-herd when many hosts share the same schedule
  • Unit= — explicit target unit (optional if names match)
#!/usr/bin/env bash
# Create a daily backup timer pair
# Run as root

# 1. The service that does the work
cat > /etc/systemd/system/daily-backup.service << 'EOF'
[Unit]
Description=Daily Database Backup
After=network.target postgresql.service

[Service]
Type=oneshot
User=backup
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
StandardError=journal
EOF

# 2. The timer that schedules it
cat > /etc/systemd/system/daily-backup.timer << 'EOF'
[Unit]
Description=Run Daily Database Backup at 02:00

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

[Install]
WantedBy=timers.target
EOF

systemctl daemon-reload
systemctl enable --now daily-backup.timer

# Verify
systemctl list-timers daily-backup.timer

Inspecting Logs and Troubleshooting Units

systemd routes all service output to the journal, which you query with journalctl. This centralises logs from all units in one searchable store.

Essential journalctl flags for service troubleshooting:

  • -u <unit> — filter by unit name
  • -f — follow log in real time (like tail -f)
  • -n 50 — show only the last 50 lines
  • --since "1 hour ago" — limit by time
  • -p err — show only error-priority and above
  • -b — show logs from the current boot only

When a unit fails to start, the first thing to run is journalctl -u <unit> -n 30 --no-pager followed by systemctl status <unit> to capture the recent error context.

#!/usr/bin/env bash
# Troubleshooting helper: dump unit status and recent logs
# Usage: ./service-debug.sh <unit-name>

UNIT="${1:-nginx.service}"

echo "=============================="
echo "STATUS: $UNIT"
echo "=============================="
systemctl status "$UNIT" --no-pager -l

echo ""
echo "=============================="
echo "RECENT LOGS (last 50 lines): $UNIT"
echo "=============================="
journalctl -u "$UNIT" -n 50 --no-pager

echo ""
echo "=============================="
echo "ERRORS ONLY (current boot)"
echo "=============================="
journalctl -u "$UNIT" -b -p err --no-pager

Knowledge Check: systemd Unit Files

Test your understanding of systemd service and timer unit configuration.

Lesson Recap: Controlling systemd Services and Writing Unit Files

In this lesson you explored the core concepts of systemd service management and unit file authoring at a professional level. Here is a summary of what was covered:

  • systemctl basics — start, stop, restart, reload, enable, disable, status, is-active, is-enabled for scripting-friendly checks
  • Listing and filteringlist-units and list-unit-files with --type and --state flags to isolate failed or enabled services
  • Unit file anatomy — the three sections [Unit], [Service], and [Install] and their key directives
  • Service typessimple, forking, notify, oneshot, and when to use each
  • Environment and securityEnvironmentFile, NoNewPrivileges, ProtectSystem, PrivateTmp for hardened daemons
  • Timer units — pairing .timer and .service files, OnCalendar expressions, and Persistent catch-up behaviour
  • Journalctl — filtering by unit, time, boot, and priority to diagnose failures efficiently

With these skills you can manage any system service, schedule recurring tasks reliably, and author production-grade unit files for your own daemons.

Frequently asked questions

Is the “Controlling systemd Services and Writing Unit Files” lesson free?

Yes — the full text of “Controlling systemd Services and Writing Unit Files” is free to read here on the web, and the DevOps Bootcamp course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DevOps Bootcamp course, upgrade to CoddyKit PRO.

What will I learn in “Controlling systemd Services and Writing Unit Files”?

Drive services with systemctl and author custom unit and timer files for scripted daemons. You practise DevOps Bootcamp with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start DevOps Bootcamp?

No prior experience is required. DevOps Bootcamp on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Controlling systemd Services and Writing Unit Files” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this DevOps Bootcamp lesson?

Yes. Every DevOps Bootcamp lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Automating User and Group Provisioning
  2. Controlling systemd Services and Writing Unit Files
  3. Disk, Filesystem, and Mount Automation
  4. Building System Health Check and Alert Scripts
← Back to DevOps Bootcamp