systemd is the init system and service manager for virtually all modern Linux distributions. It manages the full service lifecycle, handles dependencies, activates services on-demand via socket activation, schedules tasks with timers, and provides structured logging via journald. In this lab you'll write unit files from scratch, use all major unit types, analyze boot performance, and apply runtime overrides with drop-ins.
⚠️ Container note: systemd is not running as PID 1 in a plain Docker container. We'll demonstrate unit file authoring, structure, and logic — and use systemd-analyze and journalctl where possible. For full systemd behavior, use a VM or systemd-nspawn.
Step 1: Unit File Types and Structure
apt-getupdate-qq&&apt-getinstall-y-qqsystemdprocps# See all unit types availablesystemd--version|head-2# List unit files by typels/lib/systemd/system/*.service|wc-lls/lib/systemd/system/*.timer|wc-lls/lib/systemd/system/*.socket|wc-lls/lib/systemd/system/*.target|wc-lls/lib/systemd/system/*.mount|wc-lls/lib/systemd/system/*.path2>/dev/null|wc-lecho''echo"=== Example unit files ==="ls/lib/systemd/system/*.timer|head-3ls/lib/systemd/system/*.socket|head-3
📸 Verified Output:
Unit Type
Extension
Purpose
Service
.service
Long-running daemon or one-shot task
Timer
.timer
Schedule a service (cron replacement)
Socket
.socket
Socket activation — start service on first connection
Target
.target
Group units, define boot stages (like runlevels)
Mount
.mount
Mount filesystem (replaces /etc/fstab entries)
Path
.path
Trigger service on filesystem path change
Scope
.scope
Externally-created processes (Docker containers)
Slice
.slice
cgroup hierarchy node for resource management
💡 Unit files are just INI-format text files. systemd reads them from /lib/systemd/system/ (package defaults) and /etc/systemd/system/ (admin overrides — takes precedence).
Step 2: Anatomy of a Service Unit
📸 Verified Output:
Now let's write a production-quality service unit:
📸 Verified Output:
💡 Restart=on-failure restarts only on non-zero exit codes or signal deaths. Use Restart=always to restart even on clean exit (e.g., for persistent daemons). StartLimitBurst=3 prevents restart loops from hammering the system.
💡 Wants= vs Requires=: Always prefer Wants= unless the dependency is truly critical. Requires= causes your service to fail if the dependency fails — often too strict for graceful degradation.
Step 4: systemd Timers — Replacing cron
Timers are .timer unit files that activate a paired .service at scheduled times:
📸 Verified Output:
📸 Verified Output:
💡 Unlike cron, systemd timers: (1) catch up on missed runs with Persistent=true, (2) support RandomizedDelaySec to avoid thundering herd, (3) log to journald, (4) have proper dependency management.
Step 5: Socket Activation
Socket activation allows systemd to hold open a socket and only start the service when the first connection arrives:
📸 Verified Output:
💡 Advantage over traditional daemons: With socket activation, you can stop/restart a service for updates with zero dropped connections. Connections to the socket queue while the service restarts. nginx, SSH, and DBus all support this.
Step 6: journald — Structured Logging
📸 Verified Output:
📸 Verified Output:
💡 Custom fields: Services can emit structured journal fields by writing echo "MYFIELD=value" > /dev/kmsg or using sd_journal_send(). These are then queryable with journalctl MYFIELD=value.
Step 7: Drop-ins — Overriding Units Without Editing Originals
Drop-ins let you extend or override any unit file safely (survives package upgrades):
📸 Verified Output:
💡 Why clear ExecStart first?ExecStart= in a drop-in appends to the list by default. Writing ExecStart= (empty) first clears the existing value. This is required for ExecStart but not for most other keys (which replace, not append).
Step 8: Capstone — systemd-analyze Boot Performance and Full Service Stack
Scenario: The ops team reports the server boots slowly and a critical service sometimes fails. You need to: (1) analyze boot time, (2) find the slow unit, (3) fix a failing service with a proper unit file, (4) add monitoring via a timer, and (5) verify with drop-in hardening.
📸 Verified Output:
Summary
Concept
Syntax / Command
Purpose
Service unit
[Service] ExecStart=
Define daemon entrypoint
Restart policy
Restart=on-failure
Auto-restart on crash
Dependency
After=, Requires=, Wants=
Boot ordering and activation
Conditions
ConditionPathExists=
Guard — skip, not fail
Timer (calendar)
OnCalendar=*-*-* 03:00:00
Schedule by date/time
Timer (relative)
OnBootSec=5min, OnUnitActiveSec=
Schedule from boot/last-run
Socket activation
ListenStream= in .socket
On-demand service start
journald query
journalctl -u NAME -p err
Query structured logs
JSON logs
journalctl -o json-pretty
Machine-readable log fields
Drop-in override
/etc/systemd/system/NAME.d/*.conf
Extend without forking
Boot analysis
systemd-analyze blame
Find slow boot services
Critical chain
systemd-analyze critical-chain
Longest boot dependency path
Security audit
systemd-analyze security NAME
Service hardening score
Key insight: systemd timers + socket activation + drop-ins replace cron, inetd, and manual config file patching — while adding dependency tracking, journald logging, and cgroup resource limits automatically.
# Visualize the dependency types:
cat << 'EOF'
DEPENDENCY KEYWORDS:
────────────────────────────────────────────────────────────────
After=B A starts AFTER B (ordering only, no start trigger)
Before=B A starts BEFORE B
Wants=B Start B when A starts; OK if B fails
Requires=B Start B when A starts; FAIL if B fails
BindsTo=B Like Requires but also STOP A if B stops
PartOf=B Stop/restart A when B stops/restarts
CONDITION vs ASSERT:
Condition*= → false = skip unit (clean exit, no failure)
Assert*= → false = unit fails (shows as failed)
COMMON CONDITIONS:
ConditionPathExists=/path/file File must exist
ConditionACPower=true Must be on AC power
ConditionVirtualization=no Not in a VM/container
ConditionCapability=CAP_NET_ADMIN Must have capability
EOF
# Read a real multi-dependency service
cat /lib/systemd/system/networkd-dispatcher.service 2>/dev/null | head -20
DEPENDENCY KEYWORDS:
────────────────────────────────────────────────────────────────
After=B A starts AFTER B (ordering only, no start trigger)
...
[Unit]
Description=Dispatcher daemon for systemd-networkd
Documentation=https://gitlab.com/craftyguy/networkd-dispatcher
After=network.target
[Service]
Type=exec
ExecStart=/usr/bin/networkd-dispatcher \
--run-startup-triggers
Restart=on-failure
...
# Examine the apt-daily timer
cat /lib/systemd/system/apt-daily.timer
# Create our own timer
cat > /etc/systemd/system/cleanup.service << 'EOF'
[Unit]
Description=Clean temporary files
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/bin/bash -c 'find /tmp -mtime +7 -delete; echo "Cleanup done at $(date)"'
StandardOutput=journal
EOF
cat > /etc/systemd/system/cleanup.timer << 'EOF'
[Unit]
Description=Run cleanup daily at 3AM
Requires=cleanup.service
[Timer]
# Run at 3 AM every day
OnCalendar=*-*-* 03:00:00
# Run 5 min after boot (catch up if system was off)
OnBootSec=5min
# Random delay up to 30 minutes (avoid thundering herd)
RandomizedDelaySec=30min
# Store last-run time — catch up if system was offline
Persistent=true
# High accuracy (default is 1min, which is fine for daily tasks)
AccuracySec=1min
[Install]
WantedBy=timers.target
EOF
echo "=== Timer unit ==="
cat /etc/systemd/system/cleanup.timer
echo ''
echo "=== Service it activates ==="
cat /etc/systemd/system/cleanup.service
# Non-Accept mode (more common for daemons):
cat > /etc/systemd/system/webapp.socket << 'EOF'
[Unit]
Description=Web Application Socket
[Socket]
# systemd creates this socket and passes fd via $LISTEN_FDS
ListenStream=/run/webapp.sock
# OR: ListenStream=0.0.0.0:8080
SocketUser=www-data
SocketMode=0660
[Install]
WantedBy=sockets.target
EOF
echo "Socket activation benefit: webapp.service stays stopped until first connection"
echo "The socket always exists → systemctl stop webapp.service is safe"
echo "New connections queue until service starts (no ECONNREFUSED)"
# journald is systemd's log aggregator — replaces syslog
# Logs are binary, indexed, queryable
# Write to journal from a script
systemd-cat -t myapp -p info echo "Application started successfully"
systemd-cat -t myapp -p warning echo "High memory usage detected"
systemd-cat -t myapp -p err echo "Database connection failed"
# Query the journal
journalctl -t myapp --no-pager 2>/dev/null | head -10
Mar 05 06:50:00 ubuntu myapp[1234]: Application started successfully
Mar 05 06:50:00 ubuntu myapp[1234]: High memory usage detected
Mar 05 06:50:00 ubuntu myapp[1234]: Database connection failed
# Structured logging — key=value pairs
echo '=== Journal query examples ==='
echo 'journalctl -u nginx.service # logs for a specific unit'
echo 'journalctl -u nginx -f # follow (like tail -f)'
echo 'journalctl --since "1 hour ago" # time-based filter'
echo 'journalctl -p err # only errors and above'
echo 'journalctl _PID=1234 # logs from specific PID'
echo 'journalctl _SYSTEMD_UNIT=webapp.service # by unit'
echo 'journalctl -o json-pretty | head -30 # structured JSON output'
# JSON output shows all structured fields
systemd-cat -t myapp echo "Test message for JSON display"
journalctl -t myapp -o json-pretty --no-pager 2>/dev/null | head -30