Last modified: 2021-12-19 13:40

Starting a service when the Raspberry boots

One of the frequently asked question is how a service or program (the software part of your project) can be started when the Raspi boots. There's an excellent guide from Thagrol on Github (check the whole repository - interesting stuff) and there's not much to add.

Although I don't like systemd very much (short version: for most of us learning systemd is a waste of time) I see it as the go-to for service-like programs. Other recommended ways of starting service-like things are sometimes rc.local or cron.

systemd might be the correct correct way but it is also more difficult than the other solutions. Thagrol's guide lists these disadvantages:

  1. It’s more complex than cron or rc.local.
  2. Creating or modifying service files must be done as root or with sudo.
  3. The default user and group are root.
  4. The default CWD is /
  5. By default all output is discarded.
  6. By default no input is received.

They are true but it is possible to simplify things. Of course, not all: sudo will be required (but we do that anyway all day long) and you can't send input from the console to the service. For the other items I have created systemd-service. This is a shell script to make using systemd a little bit easier.

For impatient readers: running ./systemd-service without parameters gives a brief overview about options and operations.

Installation

systemd-service needs no installation. You copy (or download it) to the directory where your project lives. That's all. Let's assume that name off the directory is test-project.

$ pwd
/home/pi/work/test-project

Please notice that systemd-service is not made to be installed system-wide in e.g. /usr/bin.

The unit file

Every systemd service needs a unit file. This configures what needs to be done and how this needs to be done to start the service. There is documentation available explaining how to write unit files but systemd-service creates that file for you. You can inspect the file that would be installed with systemd-service show:

$ ./systemd-service show
[Unit]
# unit file: test-project.service
# unit path: /etc/systemd/system/test-project.service
Description=test-project service
After=network-online.target

[Service]
Type=simple
ExecStart=/home/pi/work/test-project/start.sh
# ExecStopPost=
User=pi
Restart=no

[Install]
WantedBy=multi-user.target

The first two comment lines in the [Unit] section list the unit's file- and pathname. By default systemd-service uses the directory's name for it and you should make sure that it doesn't conflict with other services (yours or system).

I have tried to choose reasonable defaults but of course you might want or need to change them. There are command line options for that:

-c cmd
changes the ExecStart command to cmd.

-n name
sets the service's name.

-p cmd
sets the ExecStopPost command.

-r
makes the service restart when it terminates.

-u
changes the user running the service.

So, if you run the command

$ ./systemd-service -c /path/to/my-program -n my-service \
    -u www-data -r -p "/bin/echo no example available" show

you will get the unit file

[Unit]
# unit file: my-service.service
# unit path: /etc/systemd/system/my-service.service
Description=my-service service
After=network-online.target

[Service]
Type=simple
ExecStart=/path/to/my-program
ExecStopPost=/bin/echo no example available
User=www-data
Restart=yes

[Install]
WantedBy=multi-user.target

Command line options are cool but here I recommend to modify parameters in the systemd-service script. First of all, you will not break anything with that. The shell script is supposed to live locally in your project directory. It's not a system-wide script. The advantage of modifying the script is that this makes the command line options permanent: Imagine you set some options and come back in one or two months? Will you remember the options you used? Or will you have written them down? In the latter case: use the script.

The systemd-service script starts with the possible options:

#
# Define permanent options/defaults here.
#

# The service name.  Default: directory name.
NAME=

# The service description.
DESCRIPTION=

# User running the service. Default: 'pi'.
USER=

# Command to run after the service was stopped.
POST_STOP=

# Command to execute.  Default: 'start.sh'
CMD=

# Set to 'yes' if the service should be restarted by systemd
# when it exists.
RESTART=

The parameters work as described (and can be overwritten by command line options).

Starting the program

So far we can construct a unit file and we will see later how it is installed. Remember the ExecStart option? It points to the file start.sh but you may not have it. That's not a problem because systemd-service creates also this script for you.

$ ./systemd-service init
-rwxr-xr-x 1 pi pi 1029 Dec 15 19:45 /home/pi/work/test-project/start.sh
Start script is /home/pi/work/test-project/start.sh.

start.sh is a short shell script that deals with some of systemd's aspects. Let's take a quick tour.

First, start.sh makes sure that the $HOME variable is properly set. systemd sets the configured user id but that's it. Everything else must be done somewhere else and the $HOME variable is one of the most important things. Then the directory $HOME/work/bin is added to the path variable.

export HOME=/home/$USER
PATH="$HOME/work/bin:$PATH"

This may not be your choice and if you want to change that you should do that in the systemd-service script. (I just made a note to add a command line option.)

The next lines change the current directory from / to your project's directory (= the directory where start.sh is) and store the directory name in the $BASE variable.

dir=`dirname $0`
cd $dir  ||  exit 1
BASE="$PWD"

After that a unique session key is created for whatever purpose. It is based on the system's date and time but notice that when start.sh is run during boot the time is not properly set and points to the past.

# Set a unique key for this run.
export SESSION_KEY=`date +'%Y%m%d-%H%M%S'`

To capture all following output a logfile is created using the session key from above and all output (normal and error) is redirected there. If you need to debug your project this file is the place to go.

# Redirect all script/program output into
# a logfile for later inspection (and removal).
progname=`basename $0 .sh`
LOG=$progname-"$SESSION_KEY".log
exec >$LOG 2>&1

Logfiles are automatically created and the script must take care that they are also removed automatically. This is done by the following lines and the comment says it all.

# Keep all logfiles from today plus 6 older logs and
# remove the others.  "Today" is difficult if the raspi
# runs without network but you should have access to the
# relevant log in case off an issue.  Even with network
# date and time may be not correct because setting time
# happens usually after the system startup.
LIST=`ls -1r $progname-*.log |
	awk '
		/./ {
			split($0, x, "-");
			if (NR == 1)
				today = x[2];
			else if (x[2] == today)
				;
			else if (n < 6)
				n++;
			else
				print $0 }'`
if [ "$LIST" != "" ]; then
    rm $LIST
fi

Then the script write some debug output to the logfile. It is to confirm the current date, directory, username and perhaps most important the environment variables. They differ very much from what you find when working in a normal shell so it might be worth to be reminded of their values. This is however not significant and if you don't like or need it you can simple remove it.

{ date; pwd; whoami;
  echo;
  env;
  echo; }

After everything is set up you start your real project program. Replace the echo statement with whatever you need to your project.

# Start the real project script.
echo no script configured.

Not necessary but an additional hint: Prefix the final command with exec to terminate the shell interpreter. It doesn't make much sense to have the shell wait for your program when there is nothing to do after it finished.

Installing the service

Ok, we have seen the unit file and a shell script that creates a basic environment to run your project. How is that installed as a service? That's the easy part:

$ sudo ./systemd-service install

creates the unit file in /etc/systemd/system (i.e. the unit path you saw in the output from ./systemd-service show) and enables it. Add all command line options you need to get the required unit file. The service will then start on next reboot or when you run

$ sudo systemctl start test-project

right now. Of course, replace test-project with the name of your service.

One advantage of systemd against rc.local or cron is that you can use systemctl to start (and stop) your service with the exact environment it would get when run at boot time. You still can run

$ ./start.sh

if you want.

To uninstall your service you run

$ sudo ./systemd-service uninstall

Do you remember the above suggestion to put options into the script and not on the command line? Here's the use case: When you install the service and assign a service name with the -n option you must give the same name when you uninstall it. Do you still remember the option from one or two months ago? Here, putting the name (if you can't or don't want to use the default) directly into the script is really handy.

Final note

That was along description of systemd-service. Run that script without parameters to get a short overview about it's operations:

Run ././systemd-service to get a brief overview.