How to Write and Manage systemd Unit Files for Custom Scripts on Linux

On
Manage systemd Unit Files for Custom Scripts on Linux

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.

Manage systemd Unit Files for Custom Scripts on Linux
📷 Create and Manage systemd Unit Files for Custom Scripts on Linux

It 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.

Read Also:
10 Useful Automation Tasks You Can Run on Ubuntu Linux

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 (journald and journalctl)
  • 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 err prints only errors and above
  • -p warning for 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 ExecStart path is wrong. Use an absolute path.
  • The required interpreter cannot be found. Search using which python3 or which 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, and NoNewPrivileges=yes
  • Keep secrets in a separate EnvironmentFile and set chmod 600

Reliability:

  • Always use Restart=on-failure on your long-running services.
  • Add a sensible RestartSec= (to avoid infinite loops)
  • Use After= and Requires= for correct dependency handling.
  • Use network-online.target (not network.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 verify before deploying a unit file.

Logging:

  • Send logs to the journal using StandardOutput=journal and StandardError=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.