systemd and Unit Files: Write Your Own Service
systemd is the first process that runs after the kernel boots, and it stays running as PID 1 forever. It manages every other long-running process on your system. The mechanism is unit files — small INI-style configs that describe how to start, stop, and supervise a service.
Where unit files live
/usr/lib/systemd/system/— units shipped by packages. Don’t edit these./etc/systemd/system/— your local units and overrides. This is where you write yours.~/.config/systemd/user/— per-user units (run withsystemctl --user).
The simplest unit file
Save as /etc/systemd/system/myapp.service:
[Unit]
Description=My App
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
Restart=on-failure
User=myapp
[Install]
WantedBy=multi-user.target
Three sections:
[Unit]— metadata and dependencies.[Service]— how to run it.[Install]— what to do when enabled (when to auto-start).
Enable and start it
sudo systemctl daemon-reload # tell systemd to reread unit files
sudo systemctl start myapp # start it now
sudo systemctl enable myapp # auto-start on boot
sudo systemctl enable --now myapp # both at once
Service types
| Type | When to use |
|---|---|
simple |
Default. Process runs in foreground; systemd treats it as ready immediately. |
forking |
Process daemonizes itself (forks and exits). systemd waits for parent exit. |
oneshot |
Runs once, exits. Use for setup scripts. |
notify |
Process tells systemd via sd_notify when ready. Most modern. |
idle |
Like simple, but waits until other jobs finish first. |
Useful directives in [Service]
ExecStart=/usr/local/bin/myapp # how to start
ExecStop=/usr/local/bin/myapp-stop # how to stop (optional)
ExecReload=/bin/kill -HUP $MAINPID # how to reload config
WorkingDirectory=/opt/myapp # cd here before running
User=myapp # run as this user
Group=myapp # this group
EnvironmentFile=/etc/myapp/env # load env vars from file
Environment="DATABASE_URL=postgres://..." # set env var
Restart=on-failure # auto-restart on crash
RestartSec=5 # wait 5s before restart
StartLimitInterval=60 # window for restart counter
StartLimitBurst=3 # max 3 restarts in 60s
StandardOutput=journal # send stdout to journalctl
StandardError=journal # same for stderr
# Security hardening
NoNewPrivileges=true # cannot gain privileges
PrivateTmp=true # private /tmp
ProtectSystem=strict # / is read-only
ProtectHome=true # /home invisible
Realistic example: Python Flask app
/etc/systemd/system/flask-api.service:
[Unit]
Description=Flask API server
After=network.target postgresql.service
Requires=postgresql.service
[Service]
Type=simple
User=apiuser
WorkingDirectory=/opt/flask-api
EnvironmentFile=/etc/flask-api/env
ExecStart=/opt/flask-api/venv/bin/gunicorn --bind 127.0.0.1:8000 app:app
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Override an existing unit (don’t edit the original)
Want to change a setting in a package-shipped service?
sudo systemctl edit nginx
# opens an editor for /etc/systemd/system/nginx.service.d/override.conf
# add only the lines you want to override:
[Service]
Restart=always
This survives package upgrades.
Timer units (better than cron)
Two files — a .timer and a .service:
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily
[Timer]
OnCalendar=daily
Persistent=true # run on boot if missed
[Install]
WantedBy=timers.target
sudo systemctl enable --now backup.timer
systemctl list-timers
Common mistakes
- Editing a package unit file directly — gets overwritten on upgrade. Use
systemctl edit. - Forgetting
daemon-reloadafter editing a unit file. - Wrong
Type=— usingsimplefor an app that forks itself, or vice versa. - Hardcoding paths in scripts but not setting
WorkingDirectory.
What to learn next
Once you can write a unit file, the next things are running it (systemctl start/stop/status) and reading its logs (journalctl). Both are coming up.