Every Linux service on a modern distribution starts, stops, restarts, logs, and runs on a schedule through one program. Learn systemctl and journalctl and you can operate almost any Linux box, whatever runs on it.

This guide covers the 20% of systemd you’ll use every day, then walks through writing a service unit and replacing a cron job with a timer. The examples were checked with systemd-analyze verify on systemd 252.

What You’ll Learn

  • What units are, and the four types worth knowing.
  • The systemctl commands you’ll run daily.
  • How to read logs with journalctl when a service misbehaves.
  • How to write a service unit and verify it before it runs.
  • How to replace a cron job with a timer.
  • How to change a vendor unit without editing its file.

The Basics

systemd is the init system on most Linux distributions: the first process the kernel starts, and the parent of everything else. It replaced a pile of shell scripts with one declarative format and one set of commands. It attracts criticism for pulling much of the boot and service surface into a single project, but it’s what ships, so it’s what you operate.

A unit is one thing systemd manages, described by a small INI-style file. Four types cover most work:

  • .service: a process to run, such as nginx.service.
  • .timer: a schedule that starts another unit, the cron replacement.
  • .socket: a port or socket that starts a service on first connection.
  • .target: a group of units, such as multi-user.target.

Units live in three places, and the first match wins:

  • /etc/systemd/system/ for your units and overrides.
  • /run/systemd/system/ for runtime units.
  • /usr/lib/systemd/system/ for units shipped by packages. Don’t edit these.

Primary Use Cases

  • Starting, stopping, and supervising services, including automatic restarts.
  • Running scheduled work with timers.
  • Collecting and querying logs through the journal.

Less Suitable Use Cases

  • Orchestrating containers across several machines. Use Kubernetes or Nomad.
  • Application-level job queues that need retries, backoff, and visibility.

When to Use systemd

Use systemd when a Linux machine must keep something running, start it at boot, run it on a schedule, or tell you why it stopped.

Everyday systemctl Commands

# What is running, what failed
systemctl list-units --type=service --state=running
systemctl --failed

# One service
systemctl status nginx
sudo systemctl start nginx
sudo systemctl stop nginx
sudo systemctl restart nginx
sudo systemctl reload nginx          # re-read config without dropping connections

# Start now and at boot, in one command
sudo systemctl enable --now nginx
sudo systemctl disable --now nginx

# Questions that answer with an exit code, for scripts
systemctl is-active nginx
systemctl is-enabled nginx

# See the actual unit file and any overrides
systemctl cat nginx

# After editing a unit file by hand
sudo systemctl daemon-reload

Two that save real time:

# Block a unit from starting at all, even as a dependency
sudo systemctl mask apache2

# What is installed, whether or not it is running
systemctl list-unit-files --type=service

restart stops and starts the process. reload asks the service to re-read its configuration and only works if the unit defines ExecReload. systemctl cat prints the unit plus every drop-in that modifies it, which is the fastest way to see what a machine actually runs.

Read Logs With journalctl

journalctl -u nginx                  # everything for one unit
journalctl -u nginx -b               # this boot only
journalctl -u nginx -f               # follow, like tail -f
journalctl -u nginx --since "30 min ago"
journalctl -p err -b                 # errors and worse, this boot
journalctl -xeu nginx                # jump to the end, with explanations
journalctl -k                        # kernel messages

# How much disk the journal uses, and how to shrink it
journalctl --disk-usage
sudo journalctl --vacuum-time=14d

journalctl -xeu <unit> is the command to reach for when a service won’t start: the -e jumps to the end, the -x adds explanatory text, and -u keeps it to the unit you care about.

Write a Service Unit

A unit that runs one script and exits, such as a backup:

# /etc/systemd/system/backup.service
[Unit]
Description=Back up /srv to object storage
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Nice=10

[Install]
WantedBy=multi-user.target

What each key does:

  • Type=oneshot tells systemd the process runs to completion instead of staying up. Long-running daemons use Type=simple (the default) or Type=notify.
  • After= and Wants=network-online.target delay the job until the network is up. After= sets the order, Wants= pulls the dependency in.
  • User= runs the job as an unprivileged account.
  • WantedBy=multi-user.target is what systemctl enable hooks into.

Check the file before you trust it:

systemd-analyze verify /etc/systemd/system/backup.service

Silence means the unit is valid. A typo gets named:

/etc/systemd/system/bad.service:8: Unknown key 'ExecStrt' in section [Service], ignoring.
bad.service: Service has no ExecStart=, ExecStop=, or SuccessAction=. Refusing.
Unit bad.service has a bad unit file setting.

Then load and run it:

sudo systemctl daemon-reload
sudo systemctl start backup.service
systemctl status backup.service

Replace a cron Job With a Timer

A timer is a unit that starts another unit on a schedule. It shares the same logs, the same status output, and the same failure handling as everything else.

# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service every night

[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl enable --now backup.timer
systemctl list-timers --all

The three keys that matter:

  • OnCalendar sets the schedule. Check an expression before committing to it:
systemd-analyze calendar "Mon *-*-* 04:00:00"
Normalized form: Mon *-*-* 04:00:00
    Next elapse: Mon 2026-09-21 04:00:00 UTC
       From now: 1 day 1h left
  • RandomizedDelaySec spreads the start time, so fifty machines don’t hit the same endpoint at 03:00:00.
  • Persistent=true runs a missed job once the machine comes back, which cron can’t do.

Timers beat cron for anything that matters: the run’s output lands in the journal, systemctl status shows the last result, and a failure can trigger another unit.

Change a Vendor Unit Without Editing It

Package upgrades overwrite files in /usr/lib/systemd/system/. Use a drop-in instead:

sudo systemctl edit nginx

That opens an empty override where you set only what you’re changing:

[Service]
Restart=always
RestartSec=5

The result lands in /etc/systemd/system/nginx.service.d/override.conf and survives upgrades. systemctl cat nginx shows the vendor unit and your override together.

Get Told When Something Fails

A failed unit is silent unless you ask to hear about it. Attach a handler with OnFailure:

# /etc/systemd/system/backup.service.d/override.conf
[Unit]
OnFailure=status-email@%n.service

How to Send System Email to Gmail From Linux has the matching status-email@.service unit and the Gmail setup it needs.

Troubleshoot a Service

systemctl --failed                   # start here
journalctl -xeu backup.service       # why it failed
systemd-analyze blame                # what made boot slow
systemd-analyze critical-chain       # the dependency path that took the time
systemctl list-dependencies nginx    # what it waits on

When a unit refuses to start, work in this order: systemctl status for the exit code, journalctl -xeu for the error, systemctl cat to confirm the unit is what you think it is, and systemd-analyze verify if you edited it.

Learn systemd: Beyond the Basics