linux / systemd

I run long-lived processes on Linux VMs as systemd services so they survive SSH logout and restart on crash or reboot. systemd also captures each service's output, so journalctl reads it back.

A service unit

This unit runs the farmer from cmd / cibot:

# /etc/systemd/system/cibot-farmer.service
[Unit]
Description=cibot farmer
After=network-online.target
Wants=network-online.target

[Service]
User=croaky
EnvironmentFile=/etc/cibot-farmer.env
ExecStart=/usr/local/bin/cibot farmer
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target

Install and start it:

sudo cp cibot-farmer.service /etc/systemd/system/
sudo systemctl enable --now cibot-farmer

enable starts it on every boot and --now starts it now. Restart=always restarts it after it exits.

Template units

A template unit, marked by the @ suffix, runs several copies of one process:

# /etc/systemd/system/[email protected]
[Service]
User=croaky
EnvironmentFile=/etc/cibot.env
ExecStart=/usr/local/bin/cibot worker
Restart=always
sudo systemctl enable --now cibot-worker@{1..8}

That starts cibot-worker@1 through @8.

Environment variables

systemd gives a process a minimal environment, so I keep config and secrets in an EnvironmentFile. The path /etc/cibot.env is my choice. Debian uses /etc/default/<name> and Red Hat uses /etc/sysconfig/<name>.

# /etc/cibot.env
FARMER_URL=http://10.50.96.89:1994
BOX_TOKEN=...
PATH=/usr/local/go/bin:/usr/local/bin:/usr/bin:/bin

The default PATH is minimal, so I set it when the process shells out to go or git. The file holds secrets, so I restrict it: sudo chown root:root /etc/cibot.env && sudo chmod 600 /etc/cibot.env.

Everyday commands

sudo systemctl restart cibot-farmer
sudo systemctl stop cibot-farmer
systemctl is-active cibot-farmer
systemctl status cibot-farmer
systemctl show cibot-farmer -p NRestarts --value

After editing a unit file, reload systemd's view before restarting:

sudo systemctl daemon-reload

Logs

systemd captures each service's stdout and stderr into its journal, so I read logs with journalctl rather than hunting for files. Every unit's output is addressable by name:

journalctl -u cibot-farmer         # everything for the unit
journalctl -u cibot-farmer -n 50   # last 50 lines
journalctl -u cibot-farmer -f      # follow live, like tail -f
journalctl -u cibot-farmer -e      # jump to the end

A template unit is addressable by instance:

journalctl -u 'cibot-worker@1'
journalctl -u 'cibot-worker@*'     # all instances

By time:

journalctl -u cibot-farmer --since "10 min ago"
journalctl -u cibot-farmer --since today
journalctl -u cibot-farmer --since "2026-07-19 17:00" --until "18:00"

By boot and severity:

journalctl -b        # since the current boot
journalctl -b -1     # the previous boot
journalctl -p err    # errors and worse, any unit

Filter with grep, or with the journal's own matching:

journalctl -u cibot-farmer | grep error
journalctl -u cibot-farmer -g connect   # journal-native regex

The service prints plain text and systemd stores it. There are no log file paths to remember and no logrotate to configure, and journalctl reads the same way for every service on the box.

← All articles