How to run a script as a service¶
You have a shell script that should run all the time: started at boot, running as an unprivileged user, and restarted automatically if it dies.
This guide turns a script into a service managed by the init system: a unit file for systemd (Debian, Ubuntu, RHEL, AlmaLinux, Rocky Linux, Arch, and Gentoo with a systemd profile), or an openrc-run script supervised by supervise-daemon on Gentoo with OpenRC.
The example script writes a timestamp to /var/lib/heartbeat/beat every 10 seconds.
Replace it with your own; the parts that matter are that the script runs in the foreground (it doesn't put itself in the background with & or nohup) and runs until it's stopped.
Before you begin¶
- You need root access through
sudo. - Know which init system runs your machine.
ps -p 1 -o comm=printssystemdon a systemd system andiniton Gentoo with OpenRC (where sysvinit is PID 1).
Steps¶
-
Create the script as
/usr/local/bin/heartbeat.sh:#!/bin/sh # heartbeat.sh: write the current time to a file every INTERVAL seconds. # Usage: heartbeat.sh [INTERVAL] set -eu interval="${1:-10}" state_dir=/var/lib/heartbeat while true; do date -Iseconds > "$state_dir/beat.tmp" mv "$state_dir/beat.tmp" "$state_dir/beat" sleep "$interval" doneMake it executable:
-
Create a system user for the service, and a state directory it owns:
Running the script as its own user limits what a bug in it can damage. If your script genuinely needs root, skip this step and leave out the user settings in the next one.
-
Define the service, start it at boot, and test that it restarts. systemd uses a unit file; OpenRC on Gentoo uses an
openrc-runservice script supervised bysupervise-daemon.Create
/etc/systemd/system/heartbeat.service:[Unit] Description=Example heartbeat writer StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=exec User=heartbeat Group=heartbeat WorkingDirectory=/var/lib/heartbeat ExecStart=/usr/local/bin/heartbeat.sh 10 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetMake systemd read the new unit, check it, then enable and start it:
sudo systemctl daemon-reload sudo systemd-analyze verify /etc/systemd/system/heartbeat.service sudo systemctl enable --now heartbeat.serviceCheck that it's running and writing its file:
$ systemctl status heartbeat.service --no-pager ● heartbeat.service - Example heartbeat writer Loaded: loaded (/etc/systemd/system/heartbeat.service; enabled; preset: enabled) Active: active (running) since Sat 2026-11-07 10:15:02 UTC; 3s ago Main PID: 1234 (heartbeat.sh) $ cat /var/lib/heartbeat/beat 2026-11-07T10:15:02+00:00Prove that it restarts after a failure: kill it, wait longer than
RestartSec=, and check the restart counter:Create
/etc/systemd/system/heartbeat.service:[Unit] Description=Example heartbeat writer StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=exec User=heartbeat Group=heartbeat WorkingDirectory=/var/lib/heartbeat ExecStart=/usr/local/bin/heartbeat.sh 10 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetMake systemd read the new unit, check it, then enable and start it:
sudo systemctl daemon-reload sudo systemd-analyze verify /etc/systemd/system/heartbeat.service sudo systemctl enable --now heartbeat.serviceCheck that it's running and writing its file:
$ systemctl status heartbeat.service --no-pager ● heartbeat.service - Example heartbeat writer Loaded: loaded (/etc/systemd/system/heartbeat.service; enabled; preset: enabled) Active: active (running) since Sat 2026-11-07 10:15:02 UTC; 3s ago Main PID: 1234 (heartbeat.sh) $ cat /var/lib/heartbeat/beat 2026-11-07T10:15:02+00:00Prove that it restarts after a failure: kill it, wait longer than
RestartSec=, and check the restart counter:$ sudo systemctl kill --signal=SIGKILL heartbeat.service $ sleep 7 $ systemctl show heartbeat.service --property=ActiveState,NRestarts ActiveState=active NRestarts=1Keep the script in
/usr/local/bin/(or/usr/bin/): SELinux lets systemd run files labelled as binaries there, but not, for example, scripts in a home directory.Create
/etc/systemd/system/heartbeat.service:[Unit] Description=Example heartbeat writer StartLimitIntervalSec=60 StartLimitBurst=5 [Service] Type=exec User=heartbeat Group=heartbeat WorkingDirectory=/var/lib/heartbeat ExecStart=/usr/local/bin/heartbeat.sh 10 Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.targetMake systemd read the new unit, check it, then enable and start it:
sudo systemctl daemon-reload sudo systemd-analyze verify /etc/systemd/system/heartbeat.service sudo systemctl enable --now heartbeat.serviceCheck that it's running and writing its file:
$ systemctl status heartbeat.service --no-pager ● heartbeat.service - Example heartbeat writer Loaded: loaded (/etc/systemd/system/heartbeat.service; enabled; preset: enabled) Active: active (running) since Sat 2026-11-07 10:15:02 UTC; 3s ago Main PID: 1234 (heartbeat.sh) $ cat /var/lib/heartbeat/beat 2026-11-07T10:15:02+00:00Prove that it restarts after a failure: kill it, wait longer than
RestartSec=, and check the restart counter:With OpenRC, create
/etc/init.d/heartbeat:#!/sbin/openrc-run # /etc/init.d/heartbeat description="Example heartbeat writer" supervisor=supervise-daemon command=/usr/local/bin/heartbeat.sh command_args="10" command_user="heartbeat:heartbeat" directory=/var/lib/heartbeat # Restart after 5 seconds; give up after 10 restarts within 5 minutes. respawn_delay=5 respawn_max=10 respawn_period=300 depend() { need localmount }supervisor=supervise-daemonkeeps the script as a child of OpenRC's supervisor, which restarts it when it exits. Without it, OpenRC starts the script and stops watching it.- Don't set
command_backgroundorpidfile: those are for daemons started bystart-stop-daemon.supervise-daemonneeds the program in the foreground and tracks it itself.
Make it executable, add it to the
defaultrunlevel so it starts at boot, and start it:sudo chmod 0755 /etc/init.d/heartbeat sudo rc-update add heartbeat default sudo rc-service heartbeat startCheck that it's running and writing its file:
$ rc-service heartbeat status * status: started $ cat /var/lib/heartbeat/beat 2026-11-07T10:15:02+00:00Prove that it restarts after a failure: kill the script (not the supervisor, which runs as root), wait, and check that a new process is running:
$ pgrep -u heartbeat -x heartbeat.sh 2345 $ sudo pkill -KILL -u heartbeat -x heartbeat.sh $ sleep 7 $ pgrep -u heartbeat -x heartbeat.sh 2391A different process ID shows that
supervise-daemonstarted it again.With a systemd profile, use the unit file and commands from the other tabs (systemd profile, not verified).
In the systemd unit:
Type=execmakessystemctl startreport an error if the script can't be executed.Restart=on-failurerestarts the script, afterRestartSec=seconds, if it exits with a non-zero status or is killed by an unclean signal such asSIGKILL. Exits caused bySIGTERM,SIGHUP,SIGINTorSIGPIPEcount as clean and aren't restarted; useRestart=alwaysto restart after any exit.StartLimitIntervalSec=60andStartLimitBurst=5stop the restarts if the service has to be started more than 5 times within 60 seconds, so a script that fails immediately doesn't loop forever. Without them, the default limit (5 starts in 10 seconds) is never reached withRestartSec=5, and systemd keeps restarting the script indefinitely.WantedBy=multi-user.targetis what makessystemctl enablestart it at boot.systemd-analyze verifyprints nothing if the unit has no problems. On some systems it also prints warnings about other, unrelated units (on Ubuntu 26.04, for example, aboutCPUAccounting=inxfs_scrubunits); you can ignore those.
The directives are described in systemd unit directives.
Verify¶
Reboot, log in again, and check that the service started by itself and the heartbeat file is current.
The two timestamps should be no more than about 10 seconds apart.
Troubleshooting¶
The service stops restarting with "Start request repeated too quickly".
The script exits straight after it starts, and systemd has reached the start limit set by StartLimitIntervalSec= and StartLimitBurst=.
Read the script's own error messages with journalctl -u heartbeat.service -n 50, fix the cause, then clear the failed state with sudo systemctl reset-failed heartbeat.service and start it again.
If you removed the start limit, the service restarts forever instead: watch systemctl show heartbeat.service -p NRestarts grow, and read the journal the same way.
On OpenRC, supervise-daemon gives up after respawn_max restarts within respawn_period; its messages go to syslog, usually /var/log/messages.
status=203/EXEC or "Permission denied".
The script isn't executable, has no #! line, or has Windows line endings (/bin/sh^M: bad interpreter).
Check with ls -l /usr/local/bin/heartbeat.sh and head -1 /usr/local/bin/heartbeat.sh | od -c.
On RHEL, AlmaLinux and Rocky Linux, also check the SELinux label with ls -Z: a script copied from a home directory may keep a user_home_t label; fix it with sudo restorecon -v /usr/local/bin/heartbeat.sh.
The script works when you run it but fails as a service.
Services start with a minimal environment: a fixed PATH, no variables from your shell profile, a different user and working directory.
Use absolute paths in the script, and set the variables it needs with Environment= in the unit, or in /etc/conf.d/heartbeat (exported with export) on OpenRC.
The service starts and is immediately "inactive (dead)" without restarting.
The script runs a background command and exits.
Make sure the long-running command is the last thing the script does and runs in the foreground; use exec for it if the script only prepares arguments.
Related¶
- Explanation: The Linux boot process
- Explanation: OpenRC and other init systems
- Reference: systemd unit directives
- How-to: How to boot into rescue mode