Run a program as a systemd service on a VPS
A systemd service keeps your program running: start on boot, restart on crash, log to the journal. How to write one, add a timer, and harden it.
What a systemd service is, and why you want one
A systemd service is a small text file that tells your server how to run a program: start it on boot, restart it if it crashes, and send its output to the system log. That is the whole job. A program you start by hand in an SSH session dies the moment you log out or the server reboots. A program wrapped in a systemd service keeps running, because the server itself owns it instead of your shell.
systemd is the init system on Ubuntu, Debian, Fedora, and most modern Linux servers. It is the first process to start and the one that supervises everything else. When you write a service file you hand your program to that supervisor. This guide shows the smallest unit that works, the three sections every unit has, how to turn it on and read its logs, how to run it on a schedule with a timer, and how to lock it down so it runs with as little privilege as possible.
The smallest service that works
A service file lives in /etc/systemd/system/, ends in .service, and needs only a few lines. Create one for a program at /usr/local/bin/myapp:
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My application
[Service]
ExecStart=/usr/local/bin/myapp
[Install]
WantedBy=multi-user.target
That is a complete, working unit. ExecStart is the command to run. WantedBy=multi-user.target means start this once the server reaches normal multi-user operation, which is what makes it come up on boot. Everything else is refinement.
The three sections, and what each is for
Every unit file is divided into sections in square brackets. A service uses three.
[Unit] describes the service and its relationships. The two lines you will use most:
[Unit]
Description=My application
After=network-online.target
Wants=network-online.target
Description is the human label you see in systemctl status. After=network-online.target tells systemd not to start your program until the network is up, which matters for anything that binds a port or makes an outbound connection.
[Service] is how the program runs. This is where most of your settings go:
[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.toml
WorkingDirectory=/opt/myapp
User=myapp
Restart=on-failure
RestartSec=5
Environment=LOG_LEVEL=info
User=myapp runs the program as an unprivileged account instead of root, which is the single most important line for safety. Restart=on-failure and RestartSec=5 get their own section below, because they are the reason most people write a service at all.
[Install] is what happens when you enable the service:
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target is what links the service into boot when you run systemctl enable. Without an [Install] section a service can be started by hand but will not come up on its own after a reboot.
Turn it on and watch it
After writing or editing any unit file, reload systemd so it reads the change, then enable and start the service in one step:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
daemon-reload is the step people forget: systemd caches unit files, so an edit does nothing until you reload. enable --now both enables the service for boot and starts it right away. Check it:
sudo systemctl status myapp.service
* myapp.service - My application
Loaded: loaded (/etc/systemd/system/myapp.service; enabled)
Active: active (running) since Wed 2026-07-15 22:40:11 UTC; 3s ago
Main PID: 4123 (myapp)
Active: active (running) and enabled are what you want. To read the program's output, ask the journal for just this unit:
sudo journalctl -u myapp.service -f
The -f follows new lines as they arrive, like tail -f. Anything your program writes to standard output or standard error lands here, with no logging setup on your part.
Restart on failure, the reason you are here
The main payoff of a service is that systemd restarts your program when it dies. Two lines do it:
[Service]
Restart=on-failure
RestartSec=5
Restart=on-failure restarts the program when it exits with a non-zero code or dies from a crash signal like SIGKILL or SIGSEGV. A clean exit, or a stop by SIGTERM, SIGINT, SIGHUP, or SIGPIPE, does not trigger it. RestartSec=5 waits five seconds between tries, so a program that crashes instantly does not spin in a tight loop. Confirm it by killing the process and watching systemd bring it back. Use SIGKILL: the default SIGTERM counts as a clean stop, so on-failure would not restart the service:
sudo systemctl kill -s SIGKILL myapp.service
sudo systemctl status myapp.service
Within five seconds the status shows a new Main PID and active (running) again. That is the whole feature, and it is why a service beats leaving a program running in tmux or screen.
Run it as an unprivileged user, and harden it
A service that runs as root can do anything to your server if the program is ever exploited. Run it as its own user, and give systemd a few directives that fence it in. First create a system account with no login and no home:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
Then set User=myapp and add hardening lines to [Service]:
[Service]
User=myapp
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
Each line removes something the program does not need. NoNewPrivileges=true stops the process from ever gaining new privileges, even through a setuid binary. PrivateTmp=true gives it a private /tmp no other process can see. ProtectSystem=strict makes the whole filesystem read-only except for a few paths you name with ReadWritePaths=. ProtectHome=true hides /home from it entirely. This is the same least-privilege thinking as putting a service behind a firewall: give it only what it needs. If you have read the guide on closing the IPv6 firewall gap on a VPS, this is the on-host half of the same idea. For a service that faces the internet, pair this hardening with Fail2ban in front of SSH and a default-deny firewall.
Rather than type all of this by hand and misremember a directive, generate a complete, hardened unit and copy it out:
Timers: the modern cron
A systemd timer runs a service on a schedule, and it is the modern replacement for a cron job. A timer is two files: a .service that does the work, and a .timer that says when. Say you want a backup at 3am every day. The service does the task once and exits:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
Type=oneshot tells systemd the program runs, finishes, and is done, rather than staying resident. The timer schedules it:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run the nightly backup
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
OnCalendar=*-*-* 03:00:00 means 3am every day. Test any calendar expression with systemd-analyze calendar "*-*-* 03:00:00", which confirms it parses and prints the next times it will fire. Persistent=true runs a missed job as soon as the server is back if it was off at 3am, which cron cannot do. Notice that a timer is enabled through timers.target, not multi-user.target. Enable the timer, not the service:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers
list-timers shows every timer with its next and last run, so you can see at a glance when your job fires next. The generator above builds the paired .service and .timer for you when you turn on timer mode. Against a cron line, a timer gives you real logs in the journal, the same hardening directives as any service, and the missed-run catch-up that Persistent=true provides. Cron is still fine for a simple job; a timer is the better tool once the job matters.
FAQ
What is the difference between a systemd service and a cron job?
A service keeps a long-running program alive: it starts on boot, restarts on failure, and logs to the journal. A cron job runs a short command on a schedule and then exits. When you want the scheduling but also want journal logs, hardening, and catch-up for missed runs, use a systemd timer, which pairs a .timer schedule with a oneshot service and replaces cron for most server tasks.
Where do I put my systemd service file?
Put your own units in /etc/systemd/system/, with a name ending in .service. That directory is for units the administrator adds, and it takes priority over units shipped by packages in /lib/systemd/system/. After creating or editing a file there, run sudo systemctl daemon-reload so systemd picks up the change.
How do I make a service restart if it crashes?
Add Restart=on-failure and RestartSec=5 to the [Service] section, then run sudo systemctl daemon-reload and restart the service. systemd relaunches the program when it exits with a non-zero code or dies from a crash signal, waiting five seconds between tries. Test it with sudo systemctl kill -s SIGKILL myapp.service — SIGTERM, the default signal, counts as a clean stop and does not trigger on-failure — and watch systemctl status show a new PID within a few seconds.
How do I run a systemd service as a non-root user?
Create a system account with sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp, then add User=myapp to the [Service] section. Add NoNewPrivileges=true, PrivateTmp=true, and ProtectSystem=strict so the process runs with as little access as it needs. Running as an unprivileged user is the most important single change you can make to a service's safety.
Why did my service fail to start?
Run systemctl status myapp.service for the summary and journalctl -u myapp.service for the full output. The most common causes are a wrong path in ExecStart, a missing WorkingDirectory, a permission error because User= cannot read a file, or a forgotten sudo systemctl daemon-reload after an edit. The journal shows the program's own error message, which usually names the problem directly.