Service manager + logging for every modern Linux box. Run agents as supervised services that restart on failure, survive reboots, and log to a queryable journal — no more
nohupand scattered logfiles.
- What Is systemd?
- Service Lifecycle (systemctl)
- Inspecting Units
- Unit Files (writing services)
- User Services (no root)
- journalctl Basics
- journalctl Filtering
- Journal Maintenance
- Timers (cron replacement)
- Analysis & Troubleshooting
- Common Workflows
- Tips & Gotchas
Init system + service manager (PID 1). Everything it manages is a unit: .service, .timer, .socket, .target, .mount. systemctl controls units; journalctl reads their logs.
sudo systemctl start nginx # Start now
sudo systemctl stop nginx # Stop now
sudo systemctl restart nginx # Stop + start
sudo systemctl reload nginx # Re-read config WITHOUT dropping connections
sudo systemctl reload-or-restart nginx # Reload if supported, else restart
systemctl status nginx # State, PID, recent log linessudo systemctl enable nginx # Start at boot
sudo systemctl enable --now nginx # Enable + start in one shot
sudo systemctl disable nginx # Don't start at boot (doesn't stop it now)
sudo systemctl mask nginx # Block ALL starts, even manual/dependency
sudo systemctl unmask nginxsystemctl is-active nginx # active / inactive / failed (scriptable)
systemctl is-enabled nginx # enabled / disabled / masked
systemctl is-failed nginxsystemctl list-units --type=service # Loaded services
systemctl list-units --type=service --state=running # Only running
systemctl list-unit-files --type=service # ALL installed + enable state
systemctl --failed # Failed units (check after boot)systemctl cat myagent # Show unit file + drop-in overrides
systemctl show myagent # Every property (add -p MainPID,MemoryCurrent)
systemctl list-dependencies myagent # What it pulls in / waits for/etc/systemd/system/ → Your units + overrides (wins)
/lib/systemd/system/ → Package-installed units (don't edit)
/run/systemd/system/ → Runtime-generated
# /etc/systemd/system/myagent.service
[Unit]
Description=Hermes agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=hermes
WorkingDirectory=/opt/agent
EnvironmentFile=/opt/agent/.env
Environment=PYTHONUNBUFFERED=1 # Logs stream to journal immediately
ExecStart=/opt/agent/venv/bin/python main.py
Restart=always # Agents: always. Crash OR clean exit → back up
RestartSec=5
# Optional caps — contain a runaway agent:
MemoryMax=512M
CPUQuota=50%
NoNewPrivileges=true
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload # REQUIRED after creating/editing unit files
sudo systemctl enable --now myagentsudo systemctl edit myagent # Drop-in override (only the keys you change)
sudo systemctl edit --full myagent # Edit a full copy
sudo systemctl revert myagent # Delete overrides, back to stockRun services under your own account — units live in ~/.config/systemd/user/.
systemctl --user daemon-reload
systemctl --user enable --now myagent
systemctl --user status myagent
journalctl --user -u myagent -f# CRITICAL for VPS agents: user services die when you log out, unless:
loginctl enable-linger $USER # Services persist with no active session
loginctl show-user $USER | grep Linger # Verify⚠️ User units use `WantedBy=default.target` in [Install], not multi-user.target.
journalctl -u myagent # All logs for a unit
journalctl -u myagent -f # Follow (tail -f equivalent)
journalctl -u myagent -n 100 # Last 100 lines
journalctl -u myagent -e # Open pager at the END
journalctl -xeu myagent # End + explanatory text — the debug defaultjournalctl --since "1 hour ago"
journalctl --since today
journalctl --since "2026-07-01 00:00" --until "2026-07-02 12:00"
journalctl -b # Current boot only
journalctl -b -1 # Previous boot (post-mortem)
journalctl --list-boots
journalctl -k # Kernel messages (dmesg equivalent)💡 Reading the system journal without sudo requires membership in the
adm or systemd-journal group: sudo usermod -aG adm $USER
journalctl -p err # Priority err and worse
journalctl -p warning..err # Priority range
journalctl -u myagent -g "timeout" # Grep within results (regex)
journalctl -u nginx -u myagent # Multiple units, interleaved
journalctl -t sshd # By syslog identifier (tag)
journalctl _PID=1234 # By field (any journal field works)
journalctl -r # Newest firstPriorities: emerg(0) alert(1) crit(2) err(3) warning(4) notice(5) info(6) debug(7)
journalctl -u myagent -o cat # Message text only (pipe-friendly)
journalctl -u myagent -o short-iso # ISO timestamps
journalctl -u myagent -o json-pretty # Full structured record
journalctl -u myagent --no-pager # For scripts/redirectsjournalctl --disk-usage # How much space logs consume
sudo journalctl --vacuum-size=500M # Shrink to 500M
sudo journalctl --vacuum-time=2weeks # Drop entries older than 2 weeks
sudo journalctl --verify # Check journal integrity# /etc/systemd/journald.conf — make limits permanent:
Storage=persistent # Survive reboots (needs /var/log/journal)
SystemMaxUse=1G
# Then:
sudo systemctl restart systemd-journald⚠️ If logs vanish on reboot: Storage=auto with no /var/log/journal dir
means volatile-only. Fix: sudo mkdir -p /var/log/journal && sudo
systemd-tmpfiles --create --prefix /var/log/journal
A timer unit triggers a matching service unit. Wins over cron: logs in the journal, Persistent= catch-up for missed runs, per-job resource caps.
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true # Run at next boot if the box was off at 3am
RandomizedDelaySec=300 # Jitter — don't stampede shared resources
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now backup.timer # Enable the TIMER, not the service
systemctl list-timers # Next/last run for all timersOnCalendar=hourly → Top of every hour
OnCalendar=daily → Midnight
OnCalendar=*:0/15 → Every 15 minutes
OnCalendar=Mon..Fri 09:00 → Weekdays at 9am
OnCalendar=*-*-01 06:00:00 → 1st of the month, 6am
systemd-analyze calendar "Mon..Fri 09:00" # Validate + show next trigger times
systemd-run --on-active=30s /usr/bin/touch /tmp/x # Ad-hoc transient timersystemd-analyze # Total boot time
systemd-analyze blame # Slowest units at boot
systemd-analyze critical-chain # Boot dependency chain
systemd-analyze verify myagent.service # Lint a unit file before deploying
sudo systemctl reset-failed # Clear failed state (also resets restart counters)sudo cp myagent.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now myagent
systemctl status myagent
journalctl -fu myagent # Watch it come upsystemctl status myagent # What state, what exit code?
journalctl -xeu myagent # Why — end of log + explanations
systemctl cat myagent # Is the unit what you think it is?
sudo systemd-analyze verify /etc/systemd/system/myagent.service
sudo systemctl daemon-reload && sudo systemctl restart myagentjournalctl -fu nginx
journalctl -fu myagent -p warning # Warnings and worse only
journalctl -f -u nginx -u myagent # Or interleaved in one pane- Edited a unit and nothing changed — you skipped
daemon-reload. systemd caches unit files; reload, then restart. enable≠start— enable wires boot-time startup only. Useenable --nowto do both; symmetric trap:disabledoesn't stop a running service.ExecStartneeds absolute paths — no shell, no$PATH, no pipes/redirection. Need shell features?ExecStart=/bin/bash -c '...'(sparingly).Restart=on-failurewon't resurrect a clean exit — an agent that exits 0 stays down. For always-on agents useRestart=always.- Rapid-crash loops hit the start limit — 5 failures in 10s and systemd gives up (
start-limit-hit).systemctl reset-failed myagentclears it; fix the crash, or setStartLimitIntervalSec=0for die-hard agents. - User services vanish after you disconnect — no linger, no service.
loginctl enable-linger $USERis step zero for user-level agents on a VPS. EnvironmentFileis not shell — noexport, no quotes needed, no variable expansion.KEY=valuelines only.- Python logs missing from the journal — stdout is block-buffered without a TTY.
Environment=PYTHONUNBUFFERED=1(orpython -u). journalctl --sinceuses local time — on UTC servers, "today" starts at UTC midnight, not yours.
Last Updated: 2026-07