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:
Descriptionis a label shown in status output.AfterandWantsmake the service wait for the network to be up.Userruns the program as a normal account, never root unless it truly needs to. A hacked program with root access owns the machine.WorkingDirectorysets the folder the program starts in, which matters for relative paths.ExecStartis the exact command, with the full path to the program. systemd does not search yourPATHthe way a shell does.Restart=on-failurerestarts it if it exits with an error or is killed.RestartSecwaits a few seconds first, so a broken program does not spin.EnvironmentFileloads settings, such as tokens, from a file, so they stay out of the service file. Keep that file private withchmod 600. See Environment variables and keeping your bot token safe.WantedBy=multi-user.targetmeans "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
| Command | What it does |
|---|---|
sudo systemctl start mybot | Start it |
sudo systemctl stop mybot | Stop it |
sudo systemctl restart mybot | Stop, then start |
sudo systemctl status mybot | Show the state |
sudo systemctl disable mybot | Do not start at boot |
journalctl -u mybot -f | Follow its log live |
journalctl -u mybot --since today | Today'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
| Problem | Likely cause |
|---|---|
status says "failed" at once | The command path is wrong, or the program errors. Read the log |
| Works by hand, fails as a service | Missing environment, wrong working directory or permissions |
| "Permission denied" | The User cannot read the files or run the program |
| Changes have no effect | You forgot daemon-reload |
| Starts, then stops | The 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.