tutorial · step 2 / 2
Write a minimal linux service file
- hardware
- Raspberry Pi 5
- os
- Raspberry Pi OS Bookworm
- verified
Let’s create a real, working service file from scratch. By the end you’ll have a Python script registered with systemd, starting at boot, and showing up cleanly in systemctl status. Every line of the service file is explained so you understand not just what to type but why it’s there.
The Application We’re Registering
For this example, suppose you have a Python script at /home/pi/my-app/main.py. The specific content doesn’t matter much for learning systemd — what matters is that it’s a long-running process (something that loops, listens for connections, or monitors sensors) rather than a script that runs once and exits.
If you want a concrete working example to follow along with, use the script from examples/python-service/hello_service.py in the GitHub repository. It’s a simple loop that logs a heartbeat message every ten seconds — straightforward enough to understand at a glance, but long-running enough to behave like a real service.
Where Service Files Live
Service files belong in /etc/systemd/system/. This directory is specifically reserved for service files you write and manage yourself — separate from the directories where systemd’s own built-in services live, and separate from the directories package managers use. Putting your files here means they won’t be overwritten by a system update, and they’ll take precedence over any lower-priority defaults.
The file must have a .service extension. The name you give it becomes the name you use with systemctl. So the convention is to choose something short and descriptive. In this tutorial I will be using my-app.service.
The Minimal Service File
Create the file with:
sudo nano /etc/systemd/system/my-app.service
Type or paste the following:
[Unit]
Description=My Application
[Service]
ExecStart=/usr/bin/python3 /home/pi/my-app/main.py
[Install]
WantedBy=multi-user.target
This is the absolute minimum. Let me explain what each line is doing.
The [Unit] Section
Description=My Application is a plain-English label. It appears whenever you inspect this service in systemctl status, in journal logs or in any system monitoring tool. If you write something vague, you or anyone else using the system won’t be able to identify the service at a glance after some time. Always write descriptive messages. For now I will be using “My Application”, but in a real project write something like “Temperature Sensor Data Pipeline” or “MQTT Edge Client”.
The [Service] Section
ExecStart=/usr/bin/python3 /home/pi/my-app/main.py is the command that launches your program. A critical rule: you must use the full, absolute path to every executable. You can’t write python3 main.py because systemd doesn’t use your shell’s PATH environment. It needs to know the exact location of the Python interpreter. You can find it with which python3 in your terminal. On most Raspberry Pi systems it’ll be /usr/bin/python3. The path to your script must also be absolute.
The [Install] Section
Systemd organises the boot process into targets. Targets are milestones that represent specific states of the system. multi-user.target represents the state where the system is fully booted, all essential hardware is initialised, networking is available, and the system is ready for normal use but before any graphical desktop starts. It essentially means “everything is ready, start normal services” point in the boot sequence. By declaring WantedBy=multi-user.target, you’re saying “start my service when the system reaches this state.” This is the correct target for the vast majority of application services.
Activating the Service
Writing the file isn’t enough on its own. Now we need to tell systemd the file exists and then enable it.
Step 1: Reload systemd’s Configuration
sudo systemctl daemon-reload
systemd reads unit files when it starts and caches their contents. When we create or modify a service file, systemd doesn’t automatically notice the change. When you run daemon-reload, it re-scans all unit file directories and updates its internal cache. Remember, you must run this every time you create or edit a service file, or your changes will be ignored.
Step 2: Enable the Service
sudo systemctl enable my-app.service
This creates a symbolic link inside the multi-user.target.wants/ directory that points to your service file. This symlink is what causes systemd to start our service at boot. If you are following this tutorial, go ahead and run this command. You can actually see the symlink after running enable.
ls -la /etc/systemd/system/multi-user.target.wants/ | grep my-app
Here is the output from my Pi:
Step 3: Start the Service Now
Enabling only affects future boots. If you want to start the service in the current session without rebooting:
sudo systemctl start my-app.service
In practice, when deploying a new service, I almost always run all three commands in sequence:
sudo systemctl daemon-reload
sudo systemctl enable my-app.service
sudo systemctl start my-app.service
Verifying it’s Running
sudo systemctl status my-app.service
If everything is working, the output will look something like this:
● my-app.service - My Application
Loaded: loaded (/etc/systemd/system/my-app.service; enabled; vendor preset: enabled)
Active: active (running) since Mon 2025-12-16 10:30:00 GMT; 5s ago
Main PID: 1234 (python3)
Tasks: 1 (limit: 4164)
Memory: 12.3M
CPU: 0.123s
CGroup: /system.slice/my-app.service
└─1234 /usr/bin/python3 /home/pi/my-app/main.py
The above is the real output from my pi.
The most important line here is Active: active (running). The word enabled next to Loaded confirms it’ll also start at boot. If you see active (dead), the service ran and exited. In this case, if you expected your script exits cleanly this is the right thing you should see. But if you expected it to keep running, this is a problem. If you see failed, the status output usually includes the last few log lines pointing to the cause. I will show you how to inspect them.
Testing Reboot Persistence
The real test of a service is whether it survives a reboot:
sudo reboot
After the Pi/Linux machine comes back up, check the status again:
sudo systemctl status my-app.service
If it shows active (running) with a start time matching the current boot, our service is working correctly as a persistent, auto-starting system service.
One-Line Enable + Start Shortcut
On newer versions of systemd (v220 and later, which includes all current Raspberry Pi OS versions), we can combine enable and start with the --now flag:
sudo systemctl enable --now my-app.service
Functionally this is identical to running enable and start separately, but just more convenient.
This Minimal Service is Still Missing Something
The service you have right now works, but it’s missing several things that matter for real-world use. If the application crashes, systemd will leave it dead and never restart it. If you’re writing relative file paths in your Python code, they’ll likely fail because systemd starts services from the root directory, not your project folder. And if you have any configuration like API keys, database passwords, and server addresses, there’s no good place to put them yet. In the next two articles I will address all of these.
Deliberately stopping or forcefully killing a service isn’t just about handling an absolute emergency. In day-to-day administration, there are several practical reasons you will need to step in and shut a process down:
- Applying Software Updates: When you modify your code, update a Python package, or download a security patch, the old version of the program keeps running in the Pi’s RAM. You must stop the service so systemd can load the fresh code.
- Conserving System Resources: The Raspberry Pi has limited RAM and CPU power. If you are about to run a heavy task like compiling code or processing video, you may need to temporarily kill background services (like a database or web server) to free up hardware memory.
- Testing and Debugging: If your script is misbehaving, it is much easier to stop the systemd service completely and run the script manually in your terminal. This allows us to see live print statements and interactive error messages in real-time.
- Changing Configuration Settings: Many applications only read their configuration or environment files exactly once when they launch. So, if you change a setting, the running service won’t notice until you stop and start it again.
- Preventing Data Corruption: If you need to back up a database or copy a live log file from your SD card, stopping the service first ensures that no new data is being written mid-copy. This prevents your backup from becoming corrupted.
Force-kill the Python process with kill <PID> and then run systemctl status to see what systemd does. Observe this and the restart policies we will be discussing in the next article would become much more concrete.
Comments