Did you write a Python script to watch how much disk space is left on your server? Or a bash script to push files to a remote server every few minutes? It runs great on the command line; you reboot your server, and it disappears. Then you have to SSH into it again, navigate to that directory, and execute the command to get it to run again. Sound familiar? This is exactly what systemd was designed for, and once you learn how to work with it, you will wonder how you ever survived without it, relying on cron job hacks. systemd is an init system and service manager present in all Linux distributions.
systemd Unit Files for Custom Scripts on LinuxIt boots up your system, starts services, and generally ensures your system stays in good shape and starts up in the correct order. But that's not all. It allows you to convert any script that you have into a system service, fully auto-starting and auto-restarting with logging capabilities, all within minutes.
This guide covers the following topics: the structure and purpose of systemd unit files, creating unit files from scratch, enabling and managing services, logging management, service dependencies, and potential problems and solutions. Whether you are a developer deploying some of your scripts onto a Linux production system, or even a hobbyist wanting to auto-start a Python script on your Raspberry Pi, this guide is for you.
What Is systemd and Why Does It Matter for Your Scripts?
Before we get to the "how" part, let's take a brief look at the "why."
systemd replaced the traditional SysV init system (the one that uses those shell scripts in /etc/init.d/ and takes a long time to start the OS). It provides a more declarative way of describing how services should run. So instead of writing lengthy shell scripts for starting, stopping, and restarting a daemon, you write a small configuration file (unit file) that describes this.
What Can a systemd Service Do for My Script?
When your script is run as a systemd service, it automatically provides:
- Automatic startup when the system boots
- Automatic restart in case of failure
- Dependency management. Your service may not be started until other services (network, database, etc.) are up.
- Centralised logging (
journaldandjournalctl) - Resource control (limiting CPU, memory, I/O usage of the service)
- Clean shutdown. systemd knows how to shut down your service properly.
- Status monitoring (check if it is running, if it has failed, or if it is crashing, etc.)
This is much better than cron or a lone nohup command.
Understanding systemd Unit Files
All things in systemd are managed using unit files. A unit file is basically a plain text file with a .service extension (if you are writing about a service unit) and gives systemd information about what your service is, how to start it, and what to do with it.
Where Do Unit Files Live?
| Directory | Purpose |
|---|---|
/lib/systemd/system/
|
Unit files installed by packages (don't touch these) |
/etc/systemd/system/
|
System-wide custom unit files (use this for root services) |
~/.config/systemd/user/
|
Per-user unit files (for services that run as a regular user) |
/run/systemd/system/
|
Runtime unit files (temporary, lost on reboot) |
When you are writing unit files for your own scripts, you'll nearly always use /etc/systemd/system/ for system-wide units, or ~/.config/systemd/user/ for user services.
The Three Sections of a Unit File
Unit files in systemd are structured into three parts:
- [Unit]: This part describes the service and its dependencies.
- [Service]: This part describes how to start, stop, and otherwise manage the service process.
- [Install]: This part states when and how to enable the service.
Think of it like a recipe. The [Unit] section is the ingredients, the [Service] section is the method, and the [Install] section says when to cook and serve.
Writing Your First systemd Service
So let's start from scratch. And let me give you a real-life example here:
We have a script named disk_monitor.py which will poll the disk usage every 60 secs and write a warning if disk usage exceeds 80%. We want to run the script as a service and start it upon boot-up and restart if it fails.
1. Create the Script
Let's create a script first: save it as /usr/local/bin/disk_monitor.py
#!/usr/bin/env python3
import shutil
import time
import logging
import sys
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
stream=sys.stdout # systemd captures stdout into the journal
)
logger = logging.getLogger(__name__)
THRESHOLD = 80 # Percentage
CHECK_INTERVAL = 60 # Seconds
def check_disk():
total, used, free = shutil.disk_usage("/")
percent_used = (used / total) * 100
if percent_used > THRESHOLD:
logger.warning(f"Disk usage is {percent_used:.1f}% — above threshold of {THRESHOLD}%!")
else:
logger.info(f"Disk usage is {percent_used:.1f}% — OK.")
if __name__ == "__main__":
logger.info("Disk Monitor started.")
while True:
check_disk()
time.sleep(CHECK_INTERVAL)
Make it executable:
sudo chmod +x /usr/local/bin/disk_monitor.py
2. Create the Unit File
Now create the service file. Open a new file with your favourite editor:
sudo nano /etc/systemd/system/disk-monitor.service
Here’s a complete, well-commented unit file for our script:
[Unit]
Description=Disk Usage Monitor
Documentation=https://your-docs-url.com
After=network.target
Wants=network.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /usr/local/bin/disk_monitor.py
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=disk-monitor
User=nobody
Group=nobody
WorkingDirectory=/tmp
[Install]
WantedBy=multi-user.target
Save and close the file. Let’s now understand what every single line does.
Dissecting the Unit File: Every Option Explained
Now let's look at the different options that are available in the systemd configuration file.
[Unit] Section
[Unit]
Description=Disk Usage Monitor
Description= is a human-readable description of your service. It will appear in the systemctl status output as well as in the journal. It should be descriptive.
Documentation=https://your-docs-url.com
This links to any useful web documentation, man page, or whatever. It supports http://, https://, man:, and file: URIs.
After=network.target
This is just saying "Start me after the network is ready"; it's not a "hard" dependency - it's only ordering that's affected. The network may not even be started, yet the service would attempt to start, just after the network.target has been processed.
Wants=network.target
This is a "soft" dependency, meaning "I require that the network be up, and start me even if the network is not ready". If the service absolutely requires that the dependency be up, it's necessary to use Requires=.
[Service] Section
[Service]
Type=simple
This dictates to systemd what type of process you are running. Some important types are:
| Type | When to Use |
|---|---|
simple
|
The process started by ExecStart is the main process. Use this for most scripts.
|
forking
|
The service forks a child process, and the parent exits. Common with traditional Unix daemons. |
oneshot
|
The process runs once and exits. Good for scripts that do a task and finish. |
notify
|
The service notifies systemd when it's ready using sd_notify(). Advanced use.
|
exec
|
Like simple, but systemd waits until the process is fully exec'd.
|
ExecStart=/usr/bin/python3 /usr/local/bin/disk_monitor.py
This is the command used to start your service. Use an absolute path for the command! systemd doesn't inherit your $PATH environment variable. Find the path for the python3 binary using which python3.
Restart=on-failure
RestartSec=10
This tells systemd to start the service automatically every time it crashes. You also specify how long to wait until it starts back up with RestartSec=. The time it's waiting before the retry. It also helps to prevent quick retry loops hammering on the system.
Options for Restart= are:
| Value | Behaviour |
|---|---|
no
|
Never restart (default) |
on-failure
|
Restart if the service exits with a non-zero exit code |
on-abnormal
|
Restart on non-zero exit, signal, or timeout |
always
|
Always restart, no matter what |
on-success
|
Restart only if the service exits cleanly (unusual) |
StandardOutput=journal
StandardError=journal
These redirect the standard output and standard error of your script to the systemd journal. To view the logs, type journalctl -u disk-monitor. The systemd service configuration manages the log management for your scripts; i.e, you don't need to log your script output to files manually.
SyslogIdentifier=disk-monitor
This is how you set the journal identity of your service. Afterwards, you can filter logs through journalctl -u disk-monitor or with a pattern like journalctl -f | grep "disk-monitor".
User=nobody
Group=nobody
These run the service as non-privileged users. It is very important for security, unless your script strictly requires it. In case you require a dedicated user and you are unable to use an existing one, you'll have to create one. A good method for creating one is: sudo useradd --system --no-create-home myservice
WorkingDirectory=/tmp
This specifies the working directory from which the ExecStart command is run. In case your scripts rely on relative paths, it would be important for systemd to set the working directory.
[Install] Section
[Install]
WantedBy=multi-user.target
WantedBy means on which target my service must be active. multi-user.target is the default operational state of a Linux system. It is roughly equivalent to runlevel 3, i.e., command-line environment with no GUI. The service will be started during boot.
Common targets include:
| Target | Meaning |
|---|---|
multi-user.target
|
Normal text-mode system (most common) |
graphical.target
|
After the GUI has started |
network-online.target
|
After the network is fully online (not just up) |
sysinit.target
|
Very early boot (rarely needed) |
Managing Your systemd Service: The Essential Commands
Now that you have your unit file, you need to make systemd aware of it and start managing it.
3. Reload systemd
Once you've created/modified a unit file, always reload the daemon:
sudo systemctl daemon-reload
The above command tells systemd to read all the unit files once again. Unless you run that, systemd will continue using old files.
4. Enable the Service
To make a service start automatically at boot time, enable it:
sudo systemctl enable disk-monitor.service
This will give you output similar to:
Created symlink /etc/systemd/system/multi-user.target.wants/disk-monitor.service → /etc/systemd/system/disk-monitor.service.
systemd simply makes a symlink to your unit file within a .wants directory on a specific target. That's all. It won't actually start the service immediately.
5. Start the Service
You can easily start your service in the present moment with:
sudo systemctl start disk-monitor.service
Or you can start it right away, and also get it to start at boot:
sudo systemctl enable --now disk-monitor.service
Check the Service Status
This is the command you'll want to run every time you want to check the status of the service:
sudo systemctl status disk-monitor.service
Sample output:
● disk-monitor.service - Disk Usage Monitor
Loaded: loaded (/etc/systemd/system/disk-monitor.service; enabled; vendor preset: enabled)
Active: active (running) since Sun 2026-05-31 10:23:45 IST; 2min ago
Main PID: 12345 (python3)
Tasks: 1 (limit: 4915)
Memory: 8.2M
CPU: 0.12s
CGroup: /system.slice/disk-monitor.service
└─12345 /usr/bin/python3 /usr/local/bin/disk_monitor.py
May 31 10:23:45 myserver disk-monitor[12345]: Disk Monitor started.
May 31 10:23:45 myserver disk-monitor[12345]: Disk usage is 54.3% — OK.
Here's the systemd management command reference:
| Command | What It Does |
|---|---|
systemctl start <service>
|
Start the service now |
systemctl stop <service>
|
Stop the service |
systemctl restart <service>
|
Stop and start the service |
systemctl reload <service>
|
Reload config without stopping (if supported) |
systemctl enable <service>
|
Enable auto-start on boot |
systemctl disable <service>
|
Disable auto-start on boot |
systemctl status <service>
|
Show current status and recent logs |
systemctl is-active <service>
|
Returns "active" or "inactive" |
systemctl is-enabled <service>
|
Returns "enabled" or "disabled" |
systemctl daemon-reload
|
Reload all unit files |
systemctl list-units --type=service
|
List all active services |
systemctl list-unit-files --type=service
|
List all installed service files |
Working with Logs: journalctl
One of systemd's greatest features is centralized logging via journald. Every output from a service can be added to the journal without effort on your part. No need to hunt through /var/log files for an errant log message again.
Viewing journalctl Logs
# View all logs for your service.
sudo journalctl -u disk-monitor.service
# Follow logs in real time (like tail -f)
sudo journalctl -u disk-monitor.service -f
# Show only the last 50 lines.
sudo journalctl -u disk-monitor.service -n 50
# Show logs since last boot
sudo journalctl -u disk-monitor.service -b
# Show logs from a specific time window
sudo journalctl -u disk-monitor.service --since "2026-05-31 09:00:00" --until "2026-05-31 11:00:00"
# Show only error-level messages.
sudo journalctl -u disk-monitor.service -p err
Important journalctl Priority Levels
| Priority | Level | Example |
|---|---|---|
| 0 | emerg | System is unusable |
| 1 | alert | Immediate action required |
| 2 | crit | Critical condition |
| 3 | err | Error condition |
| 4 | warning | Warning condition |
| 5 | notice | Normal but significant |
| 6 | info | Informational |
| 7 | debug | Debug-level messages |
You can query for the logs associated with your service using the following switches:
-p errprints only errors and above-p warningfor warnings and above
Real World Use Cases: Different systemd Service Types
Let’s go over several real-world use cases and observe how the unit file varies from case to case.
Case 1: A "One Shot" Script ( runs one time on boot)
Suppose you have a simple bash script that sets up some static routes when you boot into the system, and nothing more. In this case, you would want to use Type=oneshot:
[Unit]
Description=Setup Custom Network Routes
After=network.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/setup-routes.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
RemainAfterExit=yes is key here: this tells systemd to keep track of the service as "active" even if the process has finished running. Otherwise your service reports as "inactive (dead)" after the script completes, which looks like it failed.
Case 2: A Node.js Web App
[Unit]
Description=My Node.js Web App
After=network.target
[Service]
Type=simple
User=nodeuser
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node /var/www/myapp/server.js
Restart=always
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Limits
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
The Environment= option is how you set environment variables for your service. Under no circumstances should you store secrets directly in the unit file.
Case 3: A Script That Needs Network to Actually Be Up
The network.target merely states that the system is configured to reach the network. It does not mean you are actually connected to the internet. To have your script only begin once the internet is available, you would use network-online.target:
[Unit]
Description=My Network-Dependent Script
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/sync-script.sh
Restart=on-failure
RestartSec=30
[Install]
WantedBy=multi-user.target
And have the systemd-networkd-wait-online service enabled on your system, or NetworkManager-wait-online if you are using that:
sudo systemctl enable systemd-networkd-wait-online.service
Case 4: A Rust CLI Application as a Background Daemon
If you want to run a compiled Rust binary as a daemon, Here's how to do it:
[Unit]
Description=My Rust Background Service
Documentation=https://github.com/yourusername/myservice
After=network.target
[Service]
Type=simple
User=serviceuser
ExecStart=/usr/local/bin/my-rust-tool --config /etc/myservice/config.toml
Restart=on-failure
RestartSec=5
EnvironmentFile=-/etc/myservice/env
StandardOutput=journal
StandardError=journal
SyslogIdentifier=my-rust-tool
# Security hardening
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myservice
[Install]
WantedBy=multi-user.target
ProtectSystem=strict and ProtectHome=yes are systemd unit directives that add security. Your service will not be able to freely access anything on the filesystem. NoNewPrivileges=yes disallows processes from gaining escalated privileges. This is something you may want all services to have, especially in production.
Handling Environment Variables and Secrets
Hard-coding configuration in your unit file is not recommended; especially hard-coding secrets in them is disastrous, because users who are able to run systemctl cat can access this file.
Use EnvironmentFile
Make a separate file for your environment variables:
sudo nano /etc/myservice/env
DATABASE_URL=postgres://user:password@localhost/mydb
API_KEY=supersecretkey123
LOG_LEVEL=info
Make its permissions restrictive:
sudo chmod 600 /etc/myservice/env
sudo chown root:root /etc/myservice/env
Finally, reference it in your unit file like this:
[Service]
EnvironmentFile=/etc/myservice/env
ExecStart=/usr/local/bin/myservice
The - in front of EnvironmentFile=-/path/to/file means systemd will not fail if the file doesn't exist. Otherwise it's a fatal error.
Passing Individual Variables
If the use case is very simple, you could define variables like this:
[Service]
Environment="LOG_LEVEL=info"
Environment="PORT=8080"
Managing Service Dependencies
One of systemd’s most powerful features is its ability to manage relationships between services. Here’s how the important directives related to dependency management work:
Order Vs Dependency
These are two distinct concepts in systemd, and mixing them causes much pain.
Order (Before=, After=) : This directive does not create dependencies; it simply controls start order. If A uses After=B, it is simply started after B is started. A will still try to start even if B has failed.
Dependency (Requires=, Wants=, BindsTo=) : These directives create actual dependencies. If a dependency service fails, your service fails as well.
| Directive | Effect |
|---|---|
Requires=B
|
Stop this unit if B fails or stops. Hard dependency. |
Wants=B
|
This unit tries to start B, but will continue on if it fails. Soft dependency. |
BindsTo=B
|
Stop this unit if B stops. Harder than Requires. |
After=B
|
Start this unit after B, regardless of whether B started or failed. |
Before=B
|
Start this unit before B. |
Conflicts=B
|
Cannot be started at the same time as B. |
Here's a common real-world example of a web application dependent on a database:
[Unit]
Description=My Web Application
After=postgresql.service
Requires=postgresql.service
Security Hardening: Don’t Run as Root
Running services as root is a bad idea. Even if your script looks harmless, it's possible that due to a bug or a vulnerability, the attacker gains access to your whole machine via root privilege escalation. This is how it should be.
Create a Dedicated System User
sudo useradd \
--system \
--no-create-home \
--shell /usr/sbin/nologin \
--comment "My Service User" \
myservice
This is a system user with no home directory, with no shell login, and with UID < 1000. This user can only be used for running your service.
Security Directives Reference
Insert the following lines into your [Service] section:
[Service]
User=myservice
Group=myservice
# Prevent privilege escalation
NoNewPrivileges=yes
# Filesystem protection
ProtectSystem=strict # Make /usr, /boot, /etc read-only
ProtectHome=yes # No access to /home, /root, /run/user
ReadWritePaths=/var/lib/myservice # Allow writes only here
# Network namespace (extreme isolation)
# PrivateNetwork=yes # Uncomment to deny all network access
# Temp directory isolation
PrivateTmp=yes # Give the service its own /tmp
# Restrict system calls
SystemCallArchitectures=native
Diving into Advanced Techniques
Let's discuss some of the advanced techniques to run and manage systemd services.
Templated Services (running multiple services)
You might have to run more than one service from the same script (for each user, or each device, etc). For this, you need a templated service file. These files have @ in their names.
Create /etc/systemd/system/myservice@.service:
[Unit]
Description=My Service Instance for %i
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/myservice --instance %i
Restart=on-failure
User=%i
[Install]
WantedBy=multi-user.target
%i is a string that is replaced by the instance name. Run instance-specific units like this:
sudo systemctl start myservice@alice.service
sudo systemctl start myservice@bob.service
sudo systemctl enable myservice@alice.service
How to Use ExecStartPre and ExecStartPost
You can run other commands before or after your main process.
[Service]
ExecStartPre=/bin/mkdir -p /var/run/myservice
ExecStartPre=/usr/local/bin/myservice --check-config
ExecStart=/usr/local/bin/myservice
ExecStartPost=/usr/local/bin/notify-startup.sh
ExecStopPost=/bin/rm -rf /var/run/myservice
Any failure on an ExecStartPre command will cause the service not to start unless you prefix it with - which will tell systemd to ignore the return code.
Watchdog and Health Checks
For Type=notify service type, you can use a watchdog timer to reset a stalled service:
[Service]
WatchdogSec=30
Restart=on-failure
Your process must use sd_notify(WATCHDOG=1), although you will typically have success with Restart=always combined with a small RestartSec value for simpler services.
Socket Activation
systemd can start your service in response to incoming connections on a socket. This is socket activation and is very useful for services that don't have to run constantly.
Create /etc/systemd/system/myservice.socket:
[Unit]
Description=My Service Socket
[Socket]
ListenStream=8080
Accept=no
[Install]
WantedBy=sockets.target
Afterwards, you can link to this unit by using Requires=myservice.socket in your service file.
User-Level Services (Running Without Root)
You won't always need root access. If the service should run as an ordinary user (personal script, user space daemon, or dev tool), you should use the systemd user instance.
User unit files should be placed in $HOME/.config/systemd/user/.
mkdir -p ~/.config/systemd/user/
nano ~/.config/systemd/user/my-user-service.service
[Unit]
Description=My Personal Script
After=default.target
[Service]
Type=simple
ExecStart=/home/youruser/scripts/my-script.py
Restart=on-failure
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
You can manage it with the --user flag:
systemctl --user daemon-reload
systemctl --user enable my-user-service.service
systemctl --user start my-user-service.service
systemctl --user status my-user-service.service
journalctl --user -u my-user-service.service -f
To start user services even without logging in to a session, you can enable lingering:
sudo loginctl enable-linger yourusername
Troubleshooting Common Problems
Let's see how to fix the most common problems related to managing unit files for systemd.
Service Fails to Start (Debugging Unit File)
First, check status:
sudo systemctl status your-service.service
Check logs for more information:
sudo journalctl -u your-service.service -n 50 --no-pager
Validate your unit file:
sudo systemd-analyze verify /etc/systemd/system/your-service.service
Common Error → 'Failed to execute program: No such file or directory'
In most of the cases, one of these two things is causing it:
- The specified
ExecStartpath is wrong. Use an absolute path. - The required interpreter cannot be found. Search using
which python3orwhich bash.
Common Error → 'Permission denied'
Make sure that:
- Your script has been set executable (
chmod +x). - The value of the directive
User=belongs to a user who has rights to read your script. - All the folders in the given path have the right permissions for this user.
Common Error → Service Starts but Stops After a Short Period
Your script is surely terminating with some error. In that case, check logs:
sudo journalctl -u your-service.service -p err
If you have set Type=simple, your script is likely expected to run indefinitely (use Type=oneshot otherwise).
Common Error → 'Failed to load unit file, file not found'
Run the systemctl daemon-reload command (as root, with sudo) whenever you add/move your unit files. systemd updates its cache with the new or modified unit files.
Check Boot Performance
# See how long each service took to start
systemd-analyze blame
# View the critical path
systemd-analyze critical-chain
# Get a total boot time breakdown
systemd-analyze
Best Practices for Writing Production-Quality Services
Now let's quickly take a look at the best practices for creating and managing services on Linux through systemd.
Security:
- Never run services as root (unless strictly required)
- Always have a separate user for your service.
- Use
ProtectSystem=strict,ProtectHome=yes, andNoNewPrivileges=yes - Keep secrets in a separate
EnvironmentFileand setchmod 600
Reliability:
- Always use
Restart=on-failureon your long-running services. - Add a sensible
RestartSec=(to avoid infinite loops) - Use
After=andRequires=for correct dependency handling. - Use
network-online.target(notnetwork.target) if your service actually requires real internet connectivity.
Maintainability:
- Always use a good and descriptive name for the service through the
Description=directive. - Add a
Documentation=link. - Use
SyslogIdentifier=to easily find log messages. - Always use absolute paths in
ExecStart= - Use
systemd-analyze verifybefore deploying a unit file.
Logging:
- Send logs to the journal using
StandardOutput=journalandStandardError=journal - Use structured logging where applicable in your scripts.
- Use proper application log levels.
Conclusion
The systemd service model offers several major benefits that make it attractive to anyone interested in providing a higher level of control over how a service is run.
Among these benefits are the ability to define a service with a three-section structure (Unit, Service, and Install), the ability to start services upon failure (to allow for automatic restarts), centralized logging of service output and errors, management of service dependencies, security sandboxing of services, and allowing systemd to handle clean shutdown of a process.