How to run your own program as a systemd service

If you start a program in an SSH session, it stops when you log out. If you start it in the background, nobody restarts it when it crashes, or when the server reboots. A systemd service fixes all of that. It starts your program at boot, restarts it if it dies, collects its logs and lets you manage it with a few standard commands. Almost every modern Linux server uses systemd, so the skill carries across everywhere.

When to use it

  • A bot, a small web app or an API that should always be running.
  • A game server you run yourself on a VPS.
  • Any long-running program that should survive logouts and reboots.

Some tools, such as PM2 for Node.js, manage processes themselves. See Run a Node.js app on your VPS with PM2 and nginx. systemd works for anything, and needs no extra software.

Write a service file

Service files live in /etc/systemd/system/. Create one, for example /etc/systemd/system/mybot.service:

[Unit]
Description=My Discord bot
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=botuser
WorkingDirectory=/home/botuser/mybot
ExecStart=/usr/bin/node /home/botuser/mybot/index.js
Restart=on-failure
RestartSec=5
EnvironmentFile=/home/botuser/mybot/.env

[Install]
WantedBy=multi-user.target

What each part does:

  • Description is a label shown in status output.
  • After and Wants make the service wait for the network to be up.
  • User runs the program as a normal account, never root unless it truly needs to. A hacked program with root access owns the machine.
  • WorkingDirectory sets the folder the program starts in, which matters for relative paths.
  • ExecStart is the exact command, with the full path to the program. systemd does not search your PATH the way a shell does.
  • Restart=on-failure restarts it if it exits with an error or is killed. RestartSec waits a few seconds first, so a broken program does not spin.
  • EnvironmentFile loads settings, such as tokens, from a file, so they stay out of the service file. Keep that file private with chmod 600. See Environment variables and keeping your bot token safe.
  • WantedBy=multi-user.target means "start this at boot".

Turn it on

sudo systemctl daemon-reload
sudo systemctl enable --now mybot
sudo systemctl status mybot

daemon-reload makes systemd read your new file. enable makes it start at boot, and --now also starts it right away. status shows whether it is running, the process ID and the last few log lines.

Everyday commands

CommandWhat it does
sudo systemctl start mybotStart it
sudo systemctl stop mybotStop it
sudo systemctl restart mybotStop, then start
sudo systemctl status mybotShow the state
sudo systemctl disable mybotDo not start at boot
journalctl -u mybot -fFollow its log live
journalctl -u mybot --since todayToday's log

After you edit the service file, always run sudo systemctl daemon-reload before restarting, or your changes are ignored.

Logs for free

Anything your program prints to standard output or standard error goes into the system journal, so there is no log file to set up. Use journalctl -u mybot to read it. How to read your server logs explains how to find the real error, and Logging and monitoring your Discord bot covers more for bots.

The journal has size limits, so very chatty programs may lose old lines. If you need long-term logs, write to a file with rotation as well.

Avoiding restart storms

A program that crashes immediately will restart every few seconds forever. systemd stops after too many restarts in a short time, which is protective, but you should still deal with the cause. For a Discord bot, a restart loop is especially bad, because each failed start counts against Discord's limits. See How to handle Discord rate limits. Using RestartSec=10 or more softens it.

Tighten the service

A few optional lines reduce the damage a compromised program can do:

NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only
PrivateTmp=true

They stop the process from gaining new privileges, make most of the system read-only to it and give it a private temporary folder. Add them one at a time, because a program that needs to write somewhere will fail if you block it. Allow specific paths with ReadWritePaths=.

Common problems

ProblemLikely cause
status says "failed" at onceThe command path is wrong, or the program errors. Read the log
Works by hand, fails as a serviceMissing environment, wrong working directory or permissions
"Permission denied"The User cannot read the files or run the program
Changes have no effectYou forgot daemon-reload
Starts, then stopsThe program exits when it has no terminal. Make sure it runs in the foreground

The last one is common: a program that daemonises itself, meaning it forks into the background, confuses Type=simple. Run it in the foreground, and let systemd do the managing.

Timers instead of cron

systemd can also run things on a schedule with timer units, which pair with a service file. They give better logs than cron, but are more verbose. For most small jobs, cron is simpler.


Still stuck? Open a ticket on Discord - run /ticket in any channel, pick a department, and include your Server ID or Invoice ID so we can jump straight in.