Systemd Services and Journalctl
Systemd is the init system on most modern Linux distros. It manages services (start, stop, restart, dependencies), captures logs (journalctl), and runs timers (Cron and Systemd Timers). For Python developers: how to wrap your app as a service and read its logs.
A minimal service unit
/etc/systemd/system/myapp.service:
[Unit]
Description=My Python web app
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp/env
ExecStart=/opt/myapp/.venv/bin/gunicorn myapp.wsgi:application --bind 0.0.0.0:8000 --workers 4
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetThree sections:
| Section | What |
|---|---|
[Unit] |
metadata + dependencies |
[Service] |
how to start/stop/restart |
[Install] |
what systemctl enable should do |
Enabling and managing
sudo systemctl daemon-reload # re-read unit files after editing
sudo systemctl enable myapp.service # start at boot
sudo systemctl start myapp.service # start now
sudo systemctl enable --now myapp.service # both at once
# detailed status + last 10 lines of logs
sudo systemctl status myapp
sudo systemctl stop myapp
sudo systemctl restart myapp
sudo systemctl reload myapp # SIGHUP — only if app supports reload
sudo systemctl disable myapp # don't start at boot (still running for now)
systemctl list-units --type=service # list active services
systemctl list-units --type=service --state=failed # failed services
systemctl list-unit-files # all known unitsThe .service extension is implied; systemctl status myapp works.
journalctl — reading logs
Systemd captures stdout/stderr from services and stores them in the journal:
journalctl -u myapp # all logs for myapp
journalctl -u myapp -f # follow (like tail -f)
journalctl -u myapp -n 100 # last 100 lines
journalctl -u myapp --since "10 min ago"
journalctl -u myapp --since today
journalctl -u myapp --since "2024-01-15 10:00" --until "2024-01-15 12:00"
journalctl -u myapp -p err # only errors and worse
journalctl -u myapp --grep "ERROR" # filter (regex)
journalctl -k # kernel messages (= dmesg)
journalctl -b # current boot
journalctl -b -1 # previous boot
journalctl --disk-usage # how much disk the journal uses
sudo journalctl --vacuum-time=7d # keep only last 7 daysJournal is binary; journalctl is the only sane way to read it. For long-term log shipping, configure rsyslog/syslog-ng/fluentd to forward.
Type — how systemd tracks the process
| Type | When to use |
|---|---|
simple |
the most common — ExecStart runs in foreground; systemd considers it started immediately |
forking |
the program forks and the parent exits (classic Unix daemon style) — needs PIDFile= |
oneshot |
runs and exits; for scripts that “do a thing and stop” (used a lot with timers) |
notify |
the service signals systemd when ready via sd_notify() — for apps that need handshake |
idle |
like simple but waits until other jobs finished (cosmetic, for boot output) |
For Python services, simple is almost always right (Gunicorn, uvicorn, your own scripts).
For “I want to know when the service is actually ready, not just when it’s started”:
Type=notify
NotifyAccess=all
ExecStart=/opt/myapp/.venv/bin/python myapp.pyThe app calls systemd.daemon.notify("READY=1") when ready. Useful for services that take a while to initialize.
Restart policies
Restart=always # restart on any exit (clean or crash)
Restart=on-failure # restart only if exit code is non-zero
Restart=on-abort # restart only on signal
Restart=no # never restart (default)
RestartSec=5 # wait 5s between restarts
StartLimitBurst=5 # allow N restarts in StartLimitInterval
StartLimitInterval=60 # window for the burst limit (seconds)Restart=on-failure + RestartSec=5 is a sane default. If the app crash-loops 5 times in 60s, systemd gives up — examine logs.
Environment
Environment="DEBUG=true"
Environment="DATABASE_URL=postgres://..."
EnvironmentFile=/etc/myapp/env # one VAR=value per line
EnvironmentFile=-/etc/myapp/env-optional # leading - = ok if missingDon’t put secrets in the unit file (world-readable in /etc/systemd/system/). Use EnvironmentFile with restricted perms (chmod 600).
Security hardening
Systemd has knobs for sandboxing services:
[Service]
User=myapp # don't run as root
Group=myapp
NoNewPrivileges=true # prevent privilege escalation
PrivateTmp=true # private /tmp
ProtectSystem=strict # /usr, /boot, /etc are read-only
ProtectHome=true # /home is invisible
ReadWritePaths=/var/lib/myapp # explicit write access
CapabilityBoundingSet= # drop all capabilities
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIXsystemd-analyze security myapp.service scores the service’s exposure (lower = more locked down).
For a Python web app, the basics (User, NoNewPrivileges, ProtectSystem, ProtectHome) cost nothing and contain damage if the app is compromised.
ExecStartPre / ExecStartPost / ExecStop
[Service]
ExecStartPre=/opt/myapp/.venv/bin/python manage.py migrate --check
ExecStart=/opt/myapp/.venv/bin/gunicorn myapp.wsgi
ExecStop=/bin/kill -TERM $MAINPID # default — usually fine
ExecStartPost=/usr/bin/curl -X POST https://... # notify on startExecStartPre is for “verify or prepare before the main process.” Failure here aborts the service start.
Resource limits
[Service]
MemoryMax=512M # OOM kill if exceeded
MemoryHigh=400M # throttle approaching limit
CPUQuota=200% # max 2 cores
TasksMax=100 # max threads/processes
LimitNOFILE=65535 # file descriptor limitUseful for services that occasionally leak memory or fork-bomb — fail safely instead of taking the host down.
Reload vs restart
reload sends SIGHUP (or whatever ExecReload= says). The service must support graceful reload — if not, it’s a no-op or crash.
restart stops then starts — drops connections, fast.
For nginx, reload is true zero-downtime. For Gunicorn, kill -HUP reloads workers gracefully:
[Service]
ExecReload=/bin/kill -HUP $MAINPIDThen sudo systemctl reload myapp does graceful reload.
User services
Per-user systemd (services run as the user, no root needed):
mkdir -p ~/.config/systemd/user
# write ~/.config/systemd/user/myapp.service
systemctl --user daemon-reload
systemctl --user enable --now myapp.service
journalctl --user -u myappUseful for personal automation, dev tools. By default, user services stop when the user logs out — loginctl enable-linger USERNAME keeps them running.
When systemd isn’t right
- Kubernetes / containers — k8s manages process lifecycle; running systemd inside containers is awkward. Use the container’s PID 1 process directly.
- Heroku / PaaS — the platform handles process management.
- Quick experiments —
tmuxornohupis faster than writing a unit file. - Cron-style “run at this time” — use systemd timers (Cron and Systemd Timers) or cron.
Common pitfalls
- Edited the unit file but didn’t
daemon-reload— systemd uses the cached version. Alwayssudo systemctl daemon-reloadafter editing. Type=simplefor a forking daemon — systemd thinks the parent is the service, the parent exits, systemd marks it failed. UseType=forkingwithPIDFile=.- Secrets in unit files — readable by anyone with read on
/etc/systemd/system/. UseEnvironmentFile=withchmod 600. - Forgetting
WorkingDirectory— relative paths in your script break. - Forgetting
daemon-reloadafter editing a.servicefile — the change isn’t picked up. - Running as root — convenience, security hole. Always create a dedicated user (
useradd -r -s /bin/false myapp).
Common interview confusions
- “Systemd is bloated.” — community opinion, not technically meaningful for “should I use it.” It’s universal on Linux distros and the standard way to manage services.
- “
reloadandrestartare the same.” — reload sends SIGHUP (or the configuredExecReload); restart stops then starts. Reload is meant to be graceful; restart drops connections. - “
enablestarts the service.” —enableonly configures the service to start at boot.enable --nowdoes both. - “systemd writes logs to
/var/log/myapp.log.” — by default, to the journal (binary, accessed viajournalctl). ConfigureStandardOutput=to send elsewhere.
Interview angle 7
- “What does a systemd unit file look like for a Python web app?” — three sections:
[Unit](description, deps),[Service](User, WorkingDirectory, ExecStart, Restart=always),[Install](WantedBy=multi-user.target). Drop in/etc/systemd/system/myapp.service,daemon-reload,enable --now. - “How do you read a service’s logs?” —
journalctl -u service-name, with-ffor tail-style follow,-n 100for last 100 lines,--since "10 min ago"for time-bounded. - “What’s
Type=simplevsType=forking?” —simpleif your process stays in the foreground (most modern apps).forkingif it self-daemonizes (parent forks and exits) — old-school daemons. - “How do you make a service auto-restart on crash?” —
Restart=on-failureandRestartSec=5. Optionally cap withStartLimitBurst/StartLimitIntervalto avoid crash-looping forever. - “How do you reload a service without dropping connections?” —
ExecReload=/bin/kill -HUP $MAINPIDin the unit, thensystemctl reload service. The app must support SIGHUP-based graceful reload (gunicorn, nginx do). - “What’s the difference between
enableandstart?” —enablemeans “start at boot”;startmeans “start now.”enable --nowdoes both. - “How do you prevent a service from running as root?” —
User=andGroup=in[Service]. Combine withNoNewPrivileges=true,ProtectSystem=strict,ProtectHome=trueto sandbox further.