Systemd is the init system and service manager for RHEL 9 — it is PID 1, the parent of every other process on the system. Understanding how to manage systemd services is foundational to every other administrative task: installing Nginx means enabling the nginx service; configuring a database means understanding its unit file; debugging a startup failure means reading service logs via journalctl. Beyond simply starting and stopping services, systemd provides timers (replacing cron for many use cases), targets (grouping units into states), socket activation, resource control via cgroups, and unit dependencies that define boot order.

This guide covers the full lifecycle of systemd service management on RHEL 9: starting, stopping, enabling, disabling, masking, writing custom unit files, creating systemd timers, using systemctl to analyse the boot sequence, and reading service logs.

Prerequisites

  • RHEL 9 server with root or sudo access

Step 1 — Fundamental Service Operations

The systemctl command is the primary interface to systemd:

# Start a service (immediately, in memory only)
systemctl start nginx

# Stop a service
systemctl stop nginx

# Restart a service (stop + start)
systemctl restart nginx

# Reload a service (send SIGHUP or use service's reload mechanism without stopping)
systemctl reload nginx

# Restart only if already running; no error if stopped
systemctl try-restart nginx

# Show service status with recent log lines
systemctl status nginx

# Enable at boot (creates a symlink in the appropriate .wants directory)
systemctl enable nginx

# Enable and start immediately in one command
systemctl enable --now nginx

# Disable at boot (removes the symlink)
systemctl disable nginx

# Mask a service (prevents it from being started by anything, even manually)
systemctl mask nginx

# Unmask
systemctl unmask nginx

Step 2 — Understand Service States

A service has two orthogonal state dimensions:

  • Active state: active (running), inactive (dead), failed, activating, deactivating
  • Enabled state: enabled (starts at boot), disabled (does not start at boot), masked (cannot be started)
# Check if a service is active (exit code 0 = active)
systemctl is-active nginx

# Check if a service is enabled
systemctl is-enabled nginx

# Check if a service has failed
systemctl is-failed nginx

Step 3 — List and Inspect Units

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

# List all failed units
systemctl list-units --failed

# List all units (including inactive)
systemctl list-units --all

# List all unit files and their enabled status
systemctl list-unit-files --type=service

# Show a unit's full configuration (after merging all overrides)
systemctl cat nginx

# Show a unit's properties
systemctl show nginx

# Show a specific property
systemctl show nginx -p MainPID

Step 4 — Write a Custom Service Unit File

Custom service unit files go in /etc/systemd/system/. This directory takes precedence over the vendor-provided files in /usr/lib/systemd/system/.

vi /etc/systemd/system/myapp.service
[Unit]
Description=My Application Server
Documentation=https://example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID

# Restart policy
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=30
StartLimitBurst=3

# Resource limits
LimitNOFILE=65536
LimitNPROC=512

# Security hardening
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
ProtectHome=true

# Environment
EnvironmentFile=-/etc/myapp/env
Environment=APP_ENV=production

# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now myapp

Step 5 — Override a Vendor Unit File (Drop-In)

Never edit files in /usr/lib/systemd/system/ directly — package updates overwrite them. Use a drop-in override instead:

# Open an editor for a drop-in (creates /etc/systemd/system/nginx.service.d/override.conf)
systemctl edit nginx

Or create the file manually:

mkdir -p /etc/systemd/system/nginx.service.d
vi /etc/systemd/system/nginx.service.d/override.conf
[Service]
# Increase the open file limit for high-traffic Nginx
LimitNOFILE=100000

# Add a CPU quota (50% of 1 core)
CPUQuota=50%
systemctl daemon-reload
systemctl restart nginx

Step 6 — Create a Systemd Timer (Replacing Cron)

Systemd timers are more reliable than cron: they log to journald, support calendar scheduling, handle missed executions (if the system was off), and run as services with full unit configuration.

Create a service unit for the task:

vi /etc/systemd/system/backup-db.service
[Unit]
Description=Database Backup

[Service]
Type=oneshot
User=postgres
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal
StandardError=journal

Create the timer unit:

vi /etc/systemd/system/backup-db.timer
[Unit]
Description=Run database backup daily at 2 AM

[Timer]
# Run daily at 2:00 AM
OnCalendar=*-*-* 02:00:00

# Run immediately if the last run was missed (e.g., server was off)
Persistent=true

# Randomise start within 30 minutes to stagger fleet-wide jobs
RandomizedDelaySec=1800

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now backup-db.timer

# View all active timers and their next trigger time
systemctl list-timers

Step 7 — Analyse Boot Performance

# Total boot time breakdown
systemd-analyze

# Per-unit blame list (time each unit added to boot)
systemd-analyze blame

# Visualise boot sequence as SVG
systemd-analyze plot > /tmp/boot.svg

# Show critical chain (units on the longest boot path)
systemd-analyze critical-chain

# Check security score of a unit (sandboxing quality)
systemd-analyze security nginx

Step 8 — Working with Systemd Targets

Targets are groups of units that define system states, similar to SysV runlevels:

# Show current target
systemctl get-default

# Change the default target (multi-user = text mode, graphical = with GUI)
systemctl set-default multi-user.target

# Switch to a different target immediately (without reboot)
systemctl isolate rescue.target

# List all available targets
systemctl list-units --type=target

Step 9 — Read Service Logs with journalctl

# Follow live logs for a service
journalctl -fu nginx

# Show all logs since last boot for a service
journalctl -u nginx -b

# Show logs since a specific time
journalctl -u nginx --since "2 hours ago"

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

# Show last 100 lines
journalctl -u nginx -n 100

Verification Checklist

# Check for failed units
systemctl list-units --failed

# Verify a specific service
systemctl status myapp

# Confirm timer is active
systemctl list-timers backup-db.timer

# Check boot time
systemd-analyze

Conclusion

You now have comprehensive knowledge of systemd service management on RHEL 9. You can start, stop, enable, and mask services, write custom unit files with security hardening, create drop-in overrides without modifying vendor files, build systemd timers as reliable cron replacements, and analyse boot sequences to identify slow-starting services.

Next steps: How to Configure sudo and Sudoers on RHEL 9, How to Use journalctl for Systemd Log Analysis on RHEL 9, and How to Schedule Automated Tasks with cron and systemd on RHEL 9.