How it works
The explanation, with examples you can run.
A service in ten lines
These were run in an Ubuntu 24.04 container booted with systemd 255 as PID 1 (a privileged container with the cgroup tree mounted, which is not how you would normally run containers). The program is a Python loop that prints and handles SIGTERM:
# /opt/hello/app.pyimport signal, sys, timedef stop(signum, frame): print("SIGTERM received, closing connections", flush=True) sys.exit(0)signal.signal(signal.SIGTERM, stop)print("hello service started", flush=True)while True: time.sleep(5) print("still working", flush=True)# /etc/systemd/system/hello.service[Unit]Description=Hello demo serviceAfter=network.target [Service]ExecStart=/usr/bin/python3 /opt/hello/app.pyRestart=on-failureUser=nobody [Install]WantedBy=multi-user.targetExecStart is the command, run directly without a shell. Restart=on-failure restarts it after a crash, a kill by signal or a non-zero exit. User= drops root. WantedBy=multi-user.target is what enable hooks into so it starts at boot.
systemctl daemon-reloadsystemctl enable --now hello.servicesystemctl status hello.service --no-pagerCreated symlink /etc/systemd/system/multi-user.target.wants/hello.service → /etc/systemd/system/hello.service.● hello.service - Hello demo service Loaded: loaded (/etc/systemd/system/hello.service; enabled; preset: enabled) Active: active (running) since Fri 2026-10-16 10:45:41 UTC; 6s ago Main PID: 172 (python3) Tasks: 1 (limit: 9395) Memory: 3.7M (peak: 3.7M) CPU: 22ms CGroup: /docker/9135673b6c71.../system.slice/hello.service └─172 /usr/bin/python3 /opt/hello/app.py(The container ID in the cgroup path is shortened, and the log lines status prints at the end are left out.) enable creates the symlink that starts it at boot; --now also starts it. The CGroup: line is the service's own cgroup, which is how systemd finds every process the service starts and where MemoryMax=, CPUQuota= and TasksMax= limits apply.
Stop, crash, read the journal
systemctl stop hello.servicejournalctl -u hello.service --no-pager | tail -6Oct 16 10:45:41 9135673b6c71 python3[172]: hello service startedOct 16 10:45:46 9135673b6c71 python3[172]: still workingOct 16 10:45:47 9135673b6c71 python3[172]: SIGTERM received, closing connectionsOct 16 10:45:47 9135673b6c71 systemd[1]: Stopping hello.service - Hello demo service...Oct 16 10:45:47 9135673b6c71 systemd[1]: hello.service: Deactivated successfully.Oct 16 10:45:47 9135673b6c71 systemd[1]: Stopped hello.service - Hello demo service.The program's print output and systemd's own messages are in one place, stamped with the unit and PID. stop sent SIGTERM; the handler ran and the process exited 0. Had it ignored the signal, systemd would have sent SIGKILL after TimeoutStopSec (systemctl show -p TimeoutStopUSec hello printed 1min 30s). Now simulate a crash:
systemctl start hellokill -9 "$(systemctl show -p MainPID --value hello)"sleep 1journalctl -u hello --no-pager | tail -3Oct 16 10:45:47 9135673b6c71 systemd[1]: hello.service: Scheduled restart job, restart counter is at 1.Oct 16 10:45:47 9135673b6c71 systemd[1]: Started hello.service - Hello demo service.Oct 16 10:45:47 9135673b6c71 python3[185]: hello service startedsystemd noticed the main process died from a signal, logged Failed with result 'signal' (one line further up) and, because of Restart=on-failure, started a new process 100 ms later (RestartSec= defaults to 100 ms). systemctl show -p NRestarts hello counts restarts, and StartLimitBurst= stops a service that crashes in a tight loop.
journalctl you will use every day
journalctl -u app -ffollows a unit's log;-n 100shows the last 100 lines.--since "10 min ago",--until,-b(this boot),-b -1(previous boot, for "why did it reboot").-p errshows only priority error and worse;-kshows kernel messages such as OOM kills.-o jsonor-o catfor scripts;--disk-usageand--vacuum-time=7dto keep its size down.
Other parts worth knowing: systemctl list-units --failed, systemctl cat app (the effective unit), systemctl edit app (a drop-in override instead of editing the package's file), timers as a replacement for cron, and Environment= / EnvironmentFile= for configuration.
On macOS
macOS uses launchd instead: services are property lists in ~/Library/LaunchAgents or /Library/LaunchDaemons, controlled with launchctl, and logs go to the unified log (/usr/bin/log show --predicate 'process == "app"' --last 1h or /usr/bin/log stream; the full path matters because zsh has a builtin called log). The concepts (declarative service definitions, supervised restarts, central logs) map across directly.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.