Unit Files
{:.gc-basic}
Basic
systemd manages the system through unit files — declarative INI-format text files that describe services, devices, mounts, timers, sockets, and more. systemd reads unit files from several locations in order of precedence:
| Path | Purpose |
|---|---|
/etc/systemd/system/ |
Local admin overrides — highest precedence |
/run/systemd/system/ |
Runtime-generated units |
/lib/systemd/system/ |
Distribution-provided units — do not edit |
/usr/lib/systemd/system/ |
Vendor units (same as above on some distros) |
Files in /etc/systemd/system/ shadow those with the same name in /lib/systemd/system/. Use systemctl edit servicename to create a drop-in override without touching the original.
Unit Types
| Extension | Manages |
|---|---|
.service |
A daemon or one-shot process |
.target |
A synchronisation point / group of units |
.socket |
A socket for socket-activated services |
.timer |
A calendar or monotonic timer |
.mount |
A filesystem mount point |
.automount |
On-demand (lazy) mount |
.device |
A kernel device (from udev) |
.path |
A filesystem path watch |
.scope |
Externally created processes (e.g., login sessions) |
.slice |
cgroup hierarchy node for resource control |
Anatomy of a .service File
# /etc/systemd/system/myapp.service
[Unit]
Description=My Embedded Application
Documentation=https://example.com/myapp/docs
# Start after networking is up
After=network-online.target
# network-online must be active, not just started
Wants=network-online.target
# If postgresql fails to start, this unit will not start either
Requires=postgresql.service
[Service]
# Type tells systemd how to track readiness:
# simple — ExecStart is the main process (default)
# forking — process double-forks; systemd tracks the parent
# notify — process calls sd_notify(READY=1) when ready
# oneshot — runs to completion (no persistent process)
# dbus — service registers a D-Bus name when ready
Type=notify
# Run as a non-root user
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
# The main executable
ExecStart=/opt/myapp/bin/myapp --config /etc/myapp/config.toml
# Graceful reload without restart (send SIGHUP)
ExecReload=/bin/kill -HUP $MAINPID
# Pre-start script (runs as root before User= drop)
ExecStartPre=/opt/myapp/bin/preflight-check.sh
# Load environment variables from a file
EnvironmentFile=-/etc/myapp/myapp.env
# Restart policy
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=60s
StartLimitBurst=3
# Resource limits
LimitNOFILE=65536
LimitMEMLOCK=infinity
# Sandboxing (recommended for production)
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
# Standard output/error go to journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
# This unit starts when multi-user.target is reached
WantedBy=multi-user.target
[Unit] Dependency Keywords
| Keyword | Meaning |
|---|---|
Requires= |
Hard dependency — if the listed unit fails, this unit fails too |
Wants= |
Soft dependency — listed unit is started alongside, failure is tolerated |
After= |
Ordering — this unit starts after the listed unit is active |
Before= |
Ordering — this unit starts before the listed unit |
BindsTo= |
Like Requires= but also stops this unit if the dependency stops |
Conflicts= |
This unit cannot run simultaneously with the listed unit |
PartOf= |
Stops/restarts with the listed unit, but not started by it |
After= and Requires= are independent — Requires=foo.service does not imply After=foo.service. Always set both if you need ordering AND hard dependency.
systemctl Commands
{:.gc-basic}
Basic
Service Lifecycle
# Start / stop / restart
systemctl start myapp.service
systemctl stop myapp.service
systemctl restart myapp.service
# Reload configuration without full restart (sends ExecReload signal)
systemctl reload myapp.service
# Restart only if already running
systemctl try-restart myapp.service
# Detailed status including last log lines
systemctl status myapp.service
Example output of systemctl status:
● myapp.service - My Embedded Application
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; vendor preset: disabled)
Active: active (running) since Fri 2026-03-13 10:42:17 UTC; 3min ago
Process: 1042 ExecStartPre=/opt/myapp/bin/preflight-check.sh (code=exited, status=0/SUCCESS)
Main PID: 1057 (myapp)
CGroup: /system.slice/myapp.service
└─1057 /opt/myapp/bin/myapp --config /etc/myapp/config.toml
Enable / Disable (Boot Persistence)
# Create symlink in .wants/ directory of the target — persists across reboots
systemctl enable myapp.service
# Enable and start in one command
systemctl enable --now myapp.service
# Remove symlink — service no longer starts at boot
systemctl disable myapp.service
# Disable and stop in one command
systemctl disable --now myapp.service
Inspecting Units
# List all active units
systemctl list-units
# List all installed unit files and their state
systemctl list-unit-files
# Filter by type
systemctl list-units --type=service
systemctl list-units --type=timer
# Quick boolean checks (exit code 0 = yes)
systemctl is-active myapp.service
systemctl is-enabled myapp.service
systemctl is-failed myapp.service
# Show all failed units
systemctl --failed
Configuration Reload
# Always run after editing or adding a unit file
# Tells systemd to re-read unit files from disk without restarting anything
systemctl daemon-reload
Viewing Logs
# Follow logs for a specific service
journalctl -u myapp.service -f
# Show logs since last boot
journalctl -u myapp.service -b
# Show last 50 lines
journalctl -u myapp.service -n 50
# Show only errors and above
journalctl -u myapp.service -p err
Targets (Replacing Runlevels)
{:.gc-mid}
Intermediate
systemd targets are named synchronization points that group units together, replacing the numeric runlevels of SysVinit.
| systemd Target | SysV Runlevel | Description |
|---|---|---|
poweroff.target |
0 | Shut down and power off |
rescue.target |
1 | Single-user mode, minimal services |
multi-user.target |
2, 3, 4 | Multi-user, network, no GUI |
graphical.target |
5 | Multi-user + display manager |
reboot.target |
6 | Reboot |
emergency.target |
— | Bare minimum, root shell, no mounts |
Dependency Chain (simplified)
sysinit.target
└── local-fs.target
└── swap.target
└── cryptsetup.target
└── basic.target
└── multi-user.target
└── graphical.target
Target Management
# Switch to a different target right now (like telinit)
systemctl isolate rescue.target
systemctl isolate multi-user.target
# Set the default target (persists across reboots)
systemctl set-default multi-user.target
systemctl get-default
# See all target dependencies
systemctl list-dependencies multi-user.target
Masking Units
Masking completely prevents a unit from being started, even as a dependency:
# Mask — symlinks the unit to /dev/null
systemctl mask bluetooth.service
# Unmask
systemctl unmask bluetooth.service
This is stronger than disable — even systemctl start will refuse a masked unit.
Minimal Embedded Target
For resource-constrained embedded systems, create a custom target that skips unnecessary services:
# /etc/systemd/system/embedded.target
[Unit]
Description=Embedded Minimal Target
Requires=sysinit.target
After=sysinit.target
AllowIsolate=yes
Timers
{:.gc-mid}
Intermediate
systemd timers replace cron with a tighter integration into the service manager — missed runs can be caught up, timers are logged in journald, and they respect service dependencies.
Timer Unit Anatomy
A timer needs a matching .service unit with the same base name:
# /etc/systemd/system/cleanup.timer
[Unit]
Description=Daily /tmp Cleanup Timer
Requires=cleanup.service
[Timer]
# Run at 03:15 every day
OnCalendar=*-*-* 03:15:00
# If the system was off at the scheduled time, run it soon after next boot
Persistent=true
# Randomize start time by up to 5 minutes (avoids thundering herd)
RandomizedDelaySec=5min
[Install]
WantedBy=timers.target
# /etc/systemd/system/cleanup.service
[Unit]
Description=Daily /tmp Cleanup
[Service]
Type=oneshot
ExecStart=/bin/find /tmp -maxdepth 1 -mtime +7 -delete
ExecStart=/bin/find /var/tmp -maxdepth 1 -mtime +30 -delete
Timer Activation Keywords
| Keyword | Triggers |
|---|---|
OnCalendar= |
Absolute time (like cron). Format: DayOfWeek Year-Month-Day Hour:Min:Sec |
OnBootSec= |
Relative to system boot (e.g., OnBootSec=10min) |
OnActiveSec= |
Relative to when the timer unit was activated |
OnUnitActiveSec= |
Relative to the last time the matching service ran |
OnUnitInactiveSec= |
Relative to when the service last became inactive |
Common Calendar Expressions
OnCalendar=daily # equivalent to *-*-* 00:00:00
OnCalendar=weekly # Monday *-*-* 00:00:00
OnCalendar=monthly # *-*-01 00:00:00
OnCalendar=hourly # *-*-* *:00:00
OnCalendar=Mon,Thu 12:00 # Monday and Thursday at noon
OnCalendar=*-*-* 02:00~03:00/30 # random between 02:00 and 03:00 every 30 min
# Verify a calendar expression
systemd-analyze calendar "Mon,Thu 12:00"
# List all active timers with next/last run times
systemctl list-timers --all
Socket Activation
{:.gc-mid}
Intermediate
Socket activation lets systemd create and hold a socket open before the backing service starts. The service is launched on first connection, and connections received while the service is starting are queued in the kernel’s socket buffer — zero connections are dropped.
How It Works
- systemd reads the
.socketunit and callssocket()+bind()+listen(). - The socket file descriptor is passed to the service process via
SD_LISTEN_FDS_START(fd 3). - The service calls
sd_listen_fds()to discover inherited sockets instead of callingsocket()itself.
Socket Unit
# /etc/systemd/system/myserver.socket
[Unit]
Description=My Server Socket
[Socket]
# TCP socket on port 8080
ListenStream=0.0.0.0:8080
# OR a Unix domain socket
# ListenStream=/run/myserver/myserver.sock
# Pass socket to service (default yes)
Accept=no
[Install]
WantedBy=sockets.target
Matching Service Unit
# /etc/systemd/system/myserver.service
[Unit]
Description=My Server (socket-activated)
Requires=myserver.socket
After=myserver.socket
[Service]
Type=notify
ExecStart=/usr/sbin/myserver
# Socket is inherited on fd 3; service must not call bind()
StandardInput=socket
Benefits for Embedded Systems
- Lazy start: services only consume RAM when first needed
- Zero-downtime restart: during restart, the socket stays open and the kernel buffers connections
- Parallel boot: all sockets are created before any service starts, eliminating dependency ordering for socket consumers
Minimal C Example — Reading the Activated Socket
#include <systemd/sd-daemon.h>
#include <unistd.h>
int main(void) {
int n = sd_listen_fds(0);
if (n < 1) {
/* Fallback: create socket manually for direct invocation */
/* ... */
}
int fd = SD_LISTEN_FDS_START; /* fd 3 */
/* Use fd for accept() or recv() */
}
Boot Optimization
{:.gc-adv}
Advanced
Measuring Boot Time
# Total time broken into firmware, loader, kernel, userspace
systemd-analyze time
# Example output:
# Startup finished in 1.203s (kernel) + 3.847s (userspace) = 5.050s
# List every service sorted by startup time
systemd-analyze blame
# Show the critical path through the dependency graph
systemd-analyze critical-chain
# Generate a SVG flame graph of the boot
systemd-analyze plot > boot.svg
Reducing Boot Time on Embedded Systems
# 1. Disable unneeded services
systemctl disable ModemManager.service
systemctl disable avahi-daemon.service
systemctl disable cups.service
systemctl disable bluetooth.service
# 2. Mask units that would otherwise be pulled in as dependencies
systemctl mask systemd-resolved.service
systemctl mask systemd-timesyncd.service # if no NTP needed
# 3. Check for slow services
systemd-analyze blame | head -20
Type=notify with sd_notify()
When a service sets Type=notify, systemd waits for the process to call sd_notify(0, "READY=1") before considering it started. This avoids the race condition where systemd marks a service “active” before it has finished initialising:
#include <systemd/sd-daemon.h>
int main(void) {
/* ... complete all initialisation ... */
sd_notify(0, "READY=1"); /* Tell systemd we are ready */
sd_notify(0, "STATUS=Listening on port 8080"); /* Optional status string */
/* Main event loop */
for (;;) {
/* ... */
/* Watchdog — must be sent within WatchdogSec= interval */
sd_notify(0, "WATCHDOG=1");
}
}
[Service]
Type=notify
WatchdogSec=30s
NotifyAccess=main # or "all" if using threads/forks
Minimal Embedded systemd Configuration
For embedded targets (e.g., a Yocto or Buildroot system), disable all non-essential features at compile time:
# meson_options.txt flags for minimal systemd build
-Dnss-myhostname=false
-Dnss-resolve=false
-Dresolve=false
-Dlogind=false
-Dmachined=false
-Dhostnamed=false
-Dtimedated=false
-Dlocaled=false
-Dcoredump=false
-Dpolkit=false
-Daudit=false
-Dselinux=false
journald
{:.gc-adv}
Advanced
systemd-journald collects log entries from kernel messages (/dev/kmsg), syslog socket, and service stdout/stderr. Entries are stored in a structured binary format with indexed fields.
Storage Paths
| Path | Condition |
|---|---|
/run/log/journal/ |
Volatile (RAM) — exists when /var/log/journal/ does not exist. Lost on reboot. |
/var/log/journal/ |
Persistent — created automatically if /var/log/journal/ exists (mkdir /var/log/journal) |
journalctl Query Reference
# All logs from current boot
journalctl -b
# Logs from previous boot
journalctl -b -1
# Kernel messages only (dmesg equivalent)
journalctl -k
# Filter by priority (emerg=0, alert=1, crit=2, err=3, warn=4, notice=5, info=6, debug=7)
journalctl -p err # errors and above
journalctl -p warning..err
# Filter by time range
journalctl --since "2026-03-13 08:00" --until "2026-03-13 09:00"
journalctl --since "1 hour ago"
# Follow in real-time
journalctl -f
# Show specific service
journalctl -u nginx.service -b
# Show structured fields
journalctl -o json-pretty -n 5
# Disk usage
journalctl --disk-usage
# Vacuum old logs
journalctl --vacuum-size=100M
journalctl --vacuum-time=7d
journald Configuration (/etc/systemd/journald.conf)
[Journal]
# Persistent storage (requires /var/log/journal/ to exist)
Storage=persistent
# Maximum disk usage by journal files
SystemMaxUse=50M
# Maximum per-boot journal size
SystemMaxFileSize=10M
# Forward to syslog socket (for legacy syslog daemons)
ForwardToSyslog=no
# Forward to kernel log buffer
ForwardToKMsg=no
# Rate limiting — drop messages if a service logs too fast
RateLimitIntervalSec=30s
RateLimitBurst=1000
# Compress entries larger than this
Compress=yes
Structured Logging from Applications
# Shell — use systemd-cat
echo "Deployment complete" | systemd-cat -t myapp -p notice
# With custom fields (key=value, uppercase keys are journal fields)
systemd-cat -t myapp << 'EOF'
BUILD_ID=a3f9b
DEPLOY_ENV=production
Application started successfully
EOF
/* C — use sd-journal API */
#include <systemd/sd-journal.h>
sd_journal_send("MESSAGE=Sensor reading: %.2f°C", temp,
"PRIORITY=%i", LOG_INFO,
"SENSOR_ID=0x%02x", sensor_id,
NULL);
Interview Q&A
{:.gc-iq}
Interview Q&A
Q1 — What is the difference between Type=forking and Type=simple?
Type=simple(the default) tells systemd that the process started byExecStartis the main service process. systemd marks the service active immediately.Type=forkingis for traditional daemons that double-fork — the parent process exits and a child becomes the daemon. systemd tracks the daemon via a PID file (PIDFile=). UsingType=simplefor a forking daemon causes problems because systemd would consider the parent’s exit as service failure.
Q2 — What is the difference between WantedBy= and RequiredBy= in [Install]?
WantedBy=multi-user.targetmeanssystemctl enablecreates a symlink inmulti-user.target.wants/. If this service fails to start, the target continues.RequiredBy=multi-user.targetcreates a symlink inmulti-user.target.requires/— if this service fails, the target itself fails. UseWantedBy=for most services;RequiredBy=only when a target truly cannot function without the service.
Q3 — How does socket activation work, and why is it useful?
systemd opens the socket and holds the file descriptor before the service starts. When a connection arrives, systemd starts the service and passes the socket fd via the
SD_LISTEN_FDS_STARTmechanism. Benefits: parallel boot (all sockets available from the start regardless of service startup order), lazy start (service only starts when first needed), and zero-downtime restart (connections queue in the kernel buffer while the service restarts).
Q4 — What is the difference between After=, Requires=, and Wants=?
After=Bmeans this unit starts only after B is active — it is purely about ordering.Requires=Bis a hard dependency — if B fails, this unit fails too, but it does not imply ordering.Wants=Bis a soft dependency — B is started alongside this unit, but B’s failure is tolerated. In practice, to express “start after B, and B must succeed”, use bothAfter=BandRequires=B.
Q5 — How do you debug a service that fails to start?
# 1. Check status and last log lines
systemctl status myapp.service
# 2. Check full journal output
journalctl -u myapp.service -b --no-pager
# 3. Check dependency chain
systemctl list-dependencies myapp.service
# 4. Run the ExecStart command manually as the service user
sudo -u myapp /opt/myapp/bin/myapp --config /etc/myapp/config.toml
# 5. Enable debug output
systemctl set-environment SYSTEMD_LOG_LEVEL=debug
systemctl daemon-reload && systemctl restart myapp.service
Q6 — Where should you place custom unit files, and why not in /lib/systemd/system/?
Custom and site-local unit files belong in
/etc/systemd/system/. Files there have higher precedence than/lib/systemd/system/and survive package upgrades that might overwrite/lib/systemd/system/files. Usesystemctl edit servicenameto create a drop-in override file at/etc/systemd/system/servicename.d/override.conf, which merges with the original rather than replacing it entirely.
Q7 — How do you make a service restart automatically and limit restart storms?
[Service]
Restart=on-failure
RestartSec=5s
# Allow at most 3 restarts in 60 seconds before giving up
StartLimitIntervalSec=60s
StartLimitBurst=3
# Action when burst limit is hit (none, reboot, poweroff, etc.)
StartLimitAction=none
After the burst limit is exceeded, the service enters the
failedstate and will not restart until manually reset withsystemctl reset-failed myapp.service.
References
{:.gc-ref}
References
| Resource | Link |
|---|---|
| systemd Official Documentation | systemd.io |
man 5 systemd.unit |
Unit file format reference |
man 5 systemd.service |
Service unit reference |
man 5 systemd.socket |
Socket unit reference |
man 5 systemd.timer |
Timer unit reference |
man 1 journalctl |
Journal query tool |
man 5 journald.conf |
journald configuration |
| Freedesktop systemd Index | freedesktop.org/software/systemd/man/ |
| sd_notify(3) | freedesktop.org/software/systemd/man/sd_notify.html |
| Bootlin systemd for Embedded | bootlin.com/training/embedded-linux |
| Lennart Poettering’s Blog | 0pointer.net/blog |