systemd Deep Dive: Services, Timers, and Logs Like a Pro

August 9, 2026 · 8 min read

You SSH into a server and run systemctl status nginx. It works — but do you know what's actually happening under the hood? systemd is the init system on virtually every modern Linux distribution (Ubuntu, Debian, RHEL, Fedora, Arch, openSUSE). It's the first process (PID 1) and it decides when everything else starts, stops, and restarts. Yet most DevOps engineers only ever use four commands: start, stop, restart, and enable.

This guide goes past those four commands: writing robust unit files, replacing cron with timers, querying the journal like a pro, and using systemd's built-in sandboxing to harden every service you run.

1. Anatomy of a Unit File

Every service on a systemd machine is described by a unit file. Here's a production-grade example for a Node.js API:

# /etc/systemd/system/my-api.service
[Unit]
Description=My API service
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
Type=simple
User=deploy
WorkingDirectory=/opt/my-api
ExecStart=/usr/bin/node /opt/my-api/server.js
Restart=on-failure
RestartSec=3
Environment=NODE_ENV=production
EnvironmentFile=/etc/my-api/env

[Install]
WantedBy=multi-user.target

The three sections matter:

After editing a unit file, always reload before restarting:

systemctl daemon-reload
systemctl restart my-api
Forgetting daemon-reload is the most common systemd mistake. systemd caches unit files — your edit silently does nothing until you reload.

2. systemctl Commands You Actually Need

Beyond the basics, these are the commands that save you during an incident:

CommandWhat it does
systemctl status my-apiState, recent log lines, and the PID — first stop when something breaks.
systemctl list-units --failedEvery failed unit. Run this first on any unknown server.
systemctl list-dependencies --reverse my-apiWhat depends on this unit — finds broken chains after an upgrade.
systemctl show my-api -p RestartInspect the effective (post-override) value of any property.
systemctl mask docker.serviceHard-disable a unit — stronger than disable; even manual starts fail.

3. Timers: The Modern cron

systemd timers are cron with three huge advantages: missed-run catching, precise scheduling, and full logging. A backup timer looks like this:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly database backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-db.sh
# If a run takes 20 minutes, don't let the next one start on top of it
ExecStart=/usr/bin/touch /var/log/backup-ran.marker

# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly at 02:30

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

[Install]
WantedBy=timers.target

Then systemctl enable --now backup.timer. Key directives:

Check timers with systemctl list-timers — it shows the last run and the next fire time, so you can verify a scheduled job actually ran.

4. journalctl: Querying the Journal

Since journald captures stdout/stderr of every service, you get centralized logs without configuring anything. The queries that matter:

# Last 100 lines, follow like tail -f
journalctl -u my-api -n 100 -f

# Everything from the last hour
journalctl -u my-api --since "1 hour ago"

# Errors only, across all services
journalctl -p err -b

# A specific PID (follow a process across restarts)
journalctl _PID=12345

# Show how much disk the journal uses
journalctl --disk-usage

# Trim old journals to 200 MB
journalctl --vacuum-size=200M

To make logs survive reboots, set Storage=persistent in /etc/systemd/journald.conf (the default on most distros, but verify on minimal images). For long-term retention, ship the journal to Loki or Elasticsearch instead of growing local disk — the systemd-journal-remote service can also forward logs to a central collector over the network.

5. Sandboxing: Hardening Services for Free

This is the underrated gem. systemd can restrict what a service can do with a few lines in the [Service] section — no seccomp tooling, no container runtime required:

[Service]
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/etc /usr
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
MemoryDenyWriteExecute=true
CapabilityBoundingSet=
DirectiveProtection
NoNewPrivileges=trueBlocks setuid binaries (sudo, su) from escalating — kills most privilege-escalation chains.
ProtectSystem=strictWhole filesystem read-only except explicitly writable paths.
PrivateTmp=trueIsolated /tmp per service — no /tmp symlink attacks.
MemoryDenyWriteExecute=trueBlocks W^X memory — stops JIT-based and shellcode exploits.
CapabilityBoundingSet=Drops every Linux capability the service doesn't need.

Test your sandbox with systemd-analyze security my-api. It prints a vulnerability score (0 = excellent, 10 = terrible) with a per-directive breakdown. Run it on every service you deploy — it's the cheapest security audit you'll ever do.

6. Debugging Boot and Service Failures

When a service won't start, work through these in order:

# 1. Why did it fail?
systemctl status my-api --no-pager

# 2. What did it print before dying?
journalctl -u my-api -n 50 --no-pager

# 3. Is the unit file even valid?
systemd-analyze verify /etc/systemd/system/my-api.service

# 4. Which dependencies are dragging the boot down?
systemd-analyze blame

# 5. Watch the whole boot sequence graphically
systemd-analyze plot > boot.svg

Two failures worth recognizing on sight: Unit my-api.service not found means you enabled a unit that doesn't exist (usually after daemon-reload was skipped), and Failed with result 'exit-code' means the process itself crashed — read the journal, not the status line.

Summary

systemd isn't just "the thing that starts services". It's a complete supervision, scheduling, logging, and sandboxing platform:

Master these four areas and you'll stop fighting your init system — and start using it as a force multiplier for reliability and security.