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 one runner from cmd / sockeye:
# /etc/systemd/system/sockeye-runner.service
[Unit]
Description=sockeye runner
After=network-online.target
Wants=network-online.target
[Service]
User=sockeye
StateDirectory=sockeye
EnvironmentFile=/etc/sockeye.env
ExecStart=/usr/local/bin/sockeye runner
Restart=always
RestartSec=2
[Install]
WantedBy=multi-user.target
Install and start it:
sudo cp sockeye-runner.service /etc/systemd/system/
sudo systemctl enable --now sockeye-runner
enable starts it on every boot and --now starts it now.
Restart=always restarts it after it exits.
The service runs as a dedicated system user, never a human account, so app state and credentials stay out of a person's home:
sudo useradd --system -d /var/lib/sockeye -m -s /usr/sbin/nologin sockeye
StateDirectory=sockeye makes systemd create /var/lib/sockeye owned
by that user if the useradd step is skipped, so the unit carries the
invariant either way.
Template units
A template unit, marked by the @ suffix, runs several copies of one
process. %i in the unit is the instance name:
# /etc/systemd/system/[email protected]
[Service]
User=sockeye
StateDirectory=sockeye
EnvironmentFile=/etc/sockeye.env
Environment=RUNNER_SLOT=%i
ExecStart=/usr/local/bin/sockeye runner
Restart=always
sudo systemctl enable --now sockeye-runner@{1..4}
That starts sockeye-runner@1 through @4. Each runner keeps its own
workspace, named for its instance.
The instance name can also be a port. The sockeye server runs two
blue/green slots, sockeye-server@1994 and sockeye-server@1995, and
its unit sets Environment=PORT=%i. A deploy boots the idle slot on
the new build and stops the old one.
Environment variables
systemd gives a process a minimal environment, so I keep config and
secrets in an EnvironmentFile. The path /etc/sockeye.env is my
choice. Debian uses /etc/default/<name> and Red Hat uses
/etc/sysconfig/<name>.
# /etc/sockeye.env
SERVER_URL=https://sockeye.example.com
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/sockeye.env && sudo chmod 600 /etc/sockeye.env.
Everyday commands
sudo systemctl restart sockeye-server@1994
sudo systemctl stop sockeye-server@1994
systemctl is-active sockeye-server@1994
systemctl status sockeye-server@1994
systemctl show sockeye-server@1994 -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 sockeye-server@1994 # everything for the unit
journalctl -u sockeye-server@1994 -n 50 # last 50 lines
journalctl -u sockeye-server@1994 -f # follow live, like tail -f
journalctl -u sockeye-server@1994 -e # jump to the end
A template unit is addressable by instance:
journalctl -u 'sockeye-runner@1'
journalctl -u 'sockeye-runner@*' # all instances
By time:
journalctl -u sockeye-server@1994 --since "10 min ago"
journalctl -u sockeye-server@1994 --since today
journalctl -u sockeye-server@1994 --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 sockeye-server@1994 | grep error
journalctl -u sockeye-server@1994 -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.