You will manage existing services and author a custom systemd unit that restarts automatically on failure. By the end you will have a resilient daemon with logs in journald and a clean enable-on-boot configuration.

Learning Objectives

  • Start, stop, enable, and inspect services with systemctl.
  • Write a custom unit with automatic restart.
  • Read service logs with journalctl.
  • Time: ~2 hours · Difficulty: Intermediate · Prereqs: a systemd-based Linux server.

Architecture Overview

Environment Setup

You will need: a Linux server with systemd and a simple program or script to run as a service.

Before you begin: confirm systemd is the init system with systemctl -version.

Prerequisites

  • A systemd-based Linux distribution (any modern Debian, Ubuntu, Fedora, RHEL).
  • Root or sudo access to install unit files.
  • A test binary or script to run as a service (/usr/local/bin/app used here).
  • Basic comfort with the terminal and text editors.

Step-by-Step Execution

01
Inspect an existing service

Understanding state and dependencies is the basis of all service management.

systemctl status ssh --no-pager && systemctl list-dependencies ssh

Expected output:

ssh.service - OpenBSD Secure Shell server
   Loaded: loaded (/lib/systemd/system/ssh.service; enabled)
   Active: active (running)

What this teaches: status shows state, main PID, and recent logs; list-dependencies reveals ordering, which explains why a service waits at boot. Note: the unit is named ssh.service on Debian/Ubuntu but sshd.service on RHEL/Fedora; adjust the command to your distro.

02
Author a custom unit with auto-restart

The Restart directive makes the daemon self-heal after a crash, key for reliability.

printf '[Unit]\nDescription=My App\n[Service]\nExecStart=/usr/local/bin/app\nRestart=on-failure\nRestartSec=3\n[Install]\nWantedBy=multi-user.target\n' | sudo tee /etc/systemd/system/app.service
[ROOT REQUIRED] Creates a restart-on-failure service unit.

Expected output:

[Unit]
Description=My App
[Service]
ExecStart=/usr/local/bin/app
Restart=on-failure
RestartSec=3
[Install]
WantedBy=multi-user.target
(unit file written)

What this teaches: A minimal unit needs [Unit], [Service] with ExecStart, and [Install]; Restart=on-failure plus RestartSec makes the daemon self-healing. Type defaults to simple, so systemd tracks the foreground process you start; use Type=forking only for daemons that background themselves.

03
Reload, enable, and start
sudo systemctl daemon-reload && sudo systemctl enable --now app.service

Expected output:

(no output on success; verify with: systemctl is-active app.service)
active

What this teaches: daemon-reload makes systemd re-read unit files; enable wires it into the boot target; --now starts it immediately too.

04
Read the logs

Expected output:

# journalctl -u app.service -n 5 --no-pager
Jun 17 09:02 srv01 systemd[1]: Started My App. Jun 17 09:02 srv01 app[5123]: listening on :8080

What this teaches: journalctl -u filters the journal to one unit's logs; stdout/stderr of the service land here automatically with no logfile config.

How It Works

systemd is PID 1 and the service manager: it parses unit files into an in-memory dependency graph, starts units in topological order, and supervises them afterwards. A service unit declares what to run (ExecStart), when (After=, WantedBy=), and how failures are handled (Restart=). This declarative model replaces fragile init scripts with state systemd can reason about.

Restart policies are the reliability core. on-failure restarts on crashes but not clean exits; always restarts regardless; RestartSec paces attempts to avoid hot loops. Combined with the journal capturing all output, a well-written unit is observable and self-healing without extra tooling.

Enablement is separate from starting: enable creates symlinks in .wants directories so the unit starts at boot, while start affects only now. systemctl is-enabled versus is-active answers the two different questions of 'will it start' and 'is it running'.

Progress So Far

Testing & Validation

sudo kill -9 $(systemctl show -p MainPID --value app.service); sleep 4; systemctl is-active app.service

After killing the process you should see active again within seconds, proving auto-restart works.

Troubleshooting

SymptomLikely causeFix
Unit not found after creating the filedaemon-reload was skippedRun systemctl daemon-reload after every unit file change
Service fails with exit 203ExecStart path wrong or not executableUse absolute paths and chmod +x the binary
Restart loop spams the journalRestart=always with an instantly-crashing ExecStartAdd RestartSec=5 and StartLimitIntervalSec/StartLimitBurst to pace attempts
Enabled but not starting at bootWantedBy target wrong or enable created no symlinkCheck systemctl is-enabled and that [Install] names multi-user.target
Environment variables missingUnits do not inherit your shell environmentSet them with Environment= lines or EnvironmentFile=
journalctl -u shows nothingStandardOutput redirected or syslog forwarding onlyKeep default journal capture; check with systemctl status for recent lines
Troubleshooting
  • Unit not found: run systemctl daemon-reload after creating the file.
  • Service flaps: add StartLimitIntervalSec/StartLimitBurst to avoid restart loops.
  • No logs: ensure the program writes to stdout/stderr, which journald captures.

Extension Ideas

  • Pair the service with a systemd timer for scheduled runs.
  • Sandbox it with directives like ProtectSystem=strict.
  • Forward journald to a central log host (see Log Management).

Key Results

  • Authored a custom unit that auto-restarts within ~3 seconds of failure.
  • Enabled the service to start on boot reliably.
  • Captured all service output in journald.
  • Proved self-healing with a kill-and-recover test.

Key Takeaways

  • Units declare what, when, and how-failures-handled; systemd supervises.
  • daemon-reload after every unit edit; enable for boot, start for now.
  • Restart=on-failure plus RestartSec gives self-healing services.
  • journalctl -u is the log view; no logfile plumbing needed.
  • Absolute ExecStart paths and Environment= lines prevent classic failures.

Practice Challenges

Challenge 1: Self-healing app

Goal: write a unit for a script that crashes randomly, prove with journalctl that systemd restarted it each time, and count restarts.

Hint: script exits 1 on a coin flip; watch systemctl show -p NRestarts.

Challenge 2: Dependency ordering

Goal: make app.service start only after network-online.target and a postgres.service, then verify the order in the boot graph.

Hint: After= plus Wants=, verified with systemd-analyze critical-chain.