From Script to Service: Writing Custom Systemd Units in Linux

From Script to Service: Writing Custom Systemd Units in Linux

Learn how to convert Linux scripts into systemd services with this comprehensive guide covering unit files, automation, and best practices.

Systemd is the backbone of most modern Linux distributions, managing services, daemons, and system resources with precision. Writing custom systemd units allows you to transform scripts into reliable, manageable services. In this guide, we’ll walk through creating custom systemd units, complete with real-world examples, to help you automate and manage your Linux tasks effectively. Whether you're running a web server or a custom Python script, this post will make the process clear, SEO-friendly, and beginner-friendly.

What is a Systemd Unit?

Systemd units are configuration files that define how services, sockets, timers, and other resources behave. For this post, we’ll focus on service units, which manage daemons or scripts as services. These files, ending in .service, live in /etc/systemd/system/ (for custom units) or /lib/systemd/system/ (for system-provided units).

By creating a custom systemd unit, you can:

  • Start and stop scripts automatically.
  • Ensure services restart on failure.
  • Manage dependencies and execution order.
  • Monitor and log service activity.

Let’s dive into crafting a custom systemd unit with practical examples.

Why Use Custom Systemd Units?

Custom systemd units bring structure to your scripts. Instead of manually running a Python script or server process, a systemd unit lets you:

  • Automate startup: Launch services at boot.
  • Handle failures: Restart services if they crash.
  • Simplify management: Use commands like systemctl start or systemctl stop.
  • Improve logging: Integrate with journalctl for easy debugging.

This is especially useful for developers and sysadmins managing web apps, cron-like tasks, or background workers.

Step-by-Step: Writing a Custom Systemd Unit

Let’s create a systemd unit for a real-world example: a Python web server using Flask. We’ll then extend it to a more complex case, like a background worker.

Step 1: Write Your Script

First, create a simple Flask app. Save this as /home/user/myapp/app.py:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from my Flask app!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8080)

This script runs a web server on port 8080. Without systemd, you’d need to run it manually (python3 app.py), and it wouldn’t restart if it crashes.

Step 2: Create the Systemd Unit File

Create a file named /etc/systemd/system/myapp.service. Here’s a basic systemd unit for the Flask app:

[Unit]
Description=My Flask Web App
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/user/myapp/app.py
WorkingDirectory=/home/user/myapp
Restart=always
User=user
Environment="PYTHONUNBUFFERED=1"

[Install]
WantedBy=multi-user.target

Let’s break down the key directives:

  • [Unit] Section:
    • Description: A human-readable description of the service.
    • After: Ensures the service starts after the network is up.
  • [Service] Section:
    • ExecStart: The command to run the script (use the full path to Python).
    • WorkingDirectory: Sets the directory where the script runs.
    • Restart: Configures restart behavior (always restarts on any failure).
    • User: Runs the service as a specific user for security.
    • Environment: Enables unbuffered output for better logging.
  • [Install] Section:
    • WantedBy: Specifies when the service should start (e.g., at boot in multi-user mode).

Step 3: Enable and Start the Service

After creating the unit file, run these commands:

  1. Reload systemd to recognize the new unit:
    sudo systemctl daemon-reload
    
  2. Enable the service to start at boot:
    sudo systemctl enable myapp.service
    
  3. Start the service:
    sudo systemctl start myapp.service
    
  4. Check the status:
    sudo systemctl status myapp.service
    

You should see output confirming the service is running. Access http://your-server-ip:8080 to verify the Flask app is live.

Step 4: Monitor and Debug

Use journalctl to view logs:

journalctl -u myapp.service

This shows output from your Flask app, helping you debug issues like crashes or misconfigurations.

Real-World Example: Background Worker

Let’s try a more complex case: a Python script that processes tasks in the background. Save this as /home/user/worker/worker.py:

import time
while True:
    print("Processing task...")
    time.sleep(10)

Create a systemd unit at /etc/systemd/system/worker.service:

[Unit]
Description=My Background Worker
After=network.target

[Service]
ExecStart=/usr/bin/python3 /home/user/worker/worker.py
WorkingDirectory=/home/user/worker
Restart=on-failure
User=user
Environment="PYTHONUNBUFFERED=1"
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Key differences:

  • Restart=on-failure: Only restarts if the script exits with an error (not on normal exit).
  • StandardOutput and StandardError: Sends output to journalctl for cleaner logging.

Run the same systemctl commands to enable and start the service. Check logs with:

journalctl -u worker.service -f

Advanced Tips for Systemd Units

  • Dependencies: Use Requires or Wants to specify other services (e.g., Requires=postgresql.service for a database-dependent app).
  • Timeouts: Add TimeoutStartSec=30 to limit startup time.
  • Resource Limits: Use MemoryMax=500M or CPUQuota=50% to restrict resource usage.
  • Timers: Pair with a .timer unit for cron-like scheduling.

Example timer for running a script daily (/etc/systemd/system/daily-task.timer):

[Unit]
Description=Run daily task

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Pair it with a service unit to execute the task.

Common Pitfalls and Fixes

  • Permission Issues: Ensure the User has access to the script and working directory.
  • Path Errors: Always use absolute paths in ExecStart and WorkingDirectory.
  • Logging: If logs aren’t showing, check StandardOutput or add Environment=PYTHONUNBUFFERED=1.
  • Service Crashes: Use Restart=always or Restart=on-failure to recover automatically.

Conclusion

Writing custom systemd units is a game-changer for Linux automation. By turning scripts into services, you gain control, reliability, and seamless integration with systemd’s ecosystem. Whether it’s a Flask web app or a background worker, the process is straightforward: write your script, create a .service file, and manage it with systemctl.


Album of the day: