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 with systemctl --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-reload after editing a unit file.
  • Wrong Type= — using simple for 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *