Innovate | Connect | Transform
ContactService statusUSD $

GICT knowledge base

What Is a systemd Service and How to Create One

Published

Linux Administration
What Is a systemd Service and How to Create One

You have seen it every time you run systemctl status nginx or systemctl restart docker. But what actually is a systemd service, and how do you make one for your own script? This guide builds a real service from scratch and shows exactly what each part does.

The problem systemd solves

Before systemd, starting a long-running program meant running it in a terminal, backgrounding it with &, or using a wrapper script that watched for crashes and restarted it. None of that handles server reboots, log rotation, or dependency ordering. If your database was not ready when your web server started, the web server crashed and stayed crashed.

systemd is a service manager that solves all of this. It starts services in the right order, restarts them when they crash, watches their output, logs their events, and can start them at boot without you being logged in. On every modern Linux VPS running Ubuntu 24.04, Debian 12, or Rocky Linux 9, systemd is already running the show.

What systemd actually is

systemd is a suite of tools. The main component is PID 1 — the first process the kernel starts after boot. Everything else on the system starts as a child of PID 1 or its descendants.

systemd works with units. A unit is a configuration file that describes something systemd manages — a service, a socket, a mount point, a timer, or a path. Service units are what most people mean when they talk about systemd. Each one is a file ending in .service that tells systemd how to run and manage a program.

Units live in two places. System units are in /lib/systemd/system/ and /etc/systemd/system/. User units are in ~/.config/systemd/user/. You write your own service files in /etc/systemd/system/.

The anatomy of a service file

A service file has three sections. Each one controls a different aspect of how the service behaves.

[Unit]

This section describes the service and its dependencies.

[Unit]
Description=A short label for your service
After=network.target
Wants=network.target

Description is what you see in systemctl status and journal logs. Keep it short and accurate.

After=network.target tells systemd to start this service only after the network is ready. If your script needs to bind to a port or reach remote hosts, this matters. Remove it if your script runs before the network is needed.

Wants=network.target is a weaker dependency. systemd tries to start your service around the same time as the target, but does not fail if the target is not available. Requires= is the strong version — if the required target fails, your service does not start.

[Service]

This section is where the actual behavior is defined. There are several type settings and they change how systemd handles the process.

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/myservice/server.py
Restart=always
RestartSec=5
User=www-data
WorkingDirectory=/opt/myservice

Type=simple means systemd starts the process and considers the service running as long as that process is alive. Use this for long-running programs like web servers.

Type=forking is for programs that fork a child process and exit the parent. systemd waits for the original process to exit before marking the service as started. Use this for legacy programs that daemonize themselves.

Type=oneshot means the process runs once and exits. systemd considers it successful if the exit code is 0. Use this for backup scripts or one-time setup tasks. Add RemainAfterExit=yes if the service should be shown as active after completion.

ExecStart is the command systemd runs. It takes the full path to the executable and any arguments. There is only one ExecStart per service — if you need multiple commands, write a shell script and point ExecStart at that.

Restart=always tells systemd to restart the service whenever it stops, whether it crashed, was killed, or exited normally. Restart=on-failure restarts only on non-zero exit codes. Restart=no never restarts.

RestartSec=5 is the delay between the process stopping and systemd restarting it. Without this, systemd restarts immediately and can create a tight crash loop that is hard to debug.

User=www-data runs the service as a non-root user. This is critical for security — if your script gets compromised, the attacker has the permissions of this user, not root.

[Install]

This section tells systemd what to do when you enable or disable the service.

[Install]
WantedBy=multi-user.target

WantedBy=multi-user.target means the service starts when the system reaches the multi-user run level — in practice, at boot. WantedBy=graphical.target is for services that need a display manager. For most server workloads, multi-user.target is correct.

Build a real service from scratch

Create a simple Python script that keeps a counter in memory and serves it over HTTP. This is a realistic example — a long-running program that listens on a port and produces log output.

Create the script:

sudo mkdir -p /opt/hitservice
sudo nano /opt/hitservice/server.py

Put this in the file:

#!/usr/bin/env python3
import time
from http.server import HTTPServer, BaseHTTPRequestHandler

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(b"Hello from a systemd service!\n")

    def log_message(self, format, *args):
        print(f"[hitservice] {args[0]}")

if __name__ == "__main__":
    print("[hitservice] Starting on port 8088")
    server = HTTPServer(("0.0.0.0", 8088), Handler)
    server.serve_forever()

Make it executable:

sudo chmod +x /opt/hitservice/server.py

Create the service file:

sudo nano /etc/systemd/system/hitservice.service

Put this in the file:

[Unit]
Description=Simple hit counter HTTP service
After=network.target

[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/hitservice/server.py
Restart=always
RestartSec=5
User=nobody
WorkingDirectory=/opt/hitservice

[Install]
WantedBy=multi-user.target

Enable, start, and verify

Reload systemd to pick up the new unit file:

sudo systemctl daemon-reload

Start the service:

sudo systemctl start hitservice

Verify it is running:

sudo systemctl status hitservice

You should see active (running) in green, with the process ID and recent log lines. Test it with curl:

sudo curl http://localhost:8088

Enable it so it starts at boot:

sudo systemctl enable hitservice

systemd creates a symlink pointing to the service file. That symlink is what triggers the service to start at boot.

Restart behavior and crash recovery

Kill the process to see restart in action:

sudo kill $(pgrep -f hitservice)

Wait a few seconds and check the status again:

sudo systemctl status hitservice

systemd will show that it restarted the service automatically, with a new process ID and a log entry recording the restart. The RestartSec=5 delay means systemd waits 5 seconds before bringing it back up, which is long enough to notice the crash and short enough that the service is back quickly.

Reading logs with journalctl

systemd routes all service output through its logging system. Read it with journalctl:

sudo journalctl -u hitservice --no-pager

The --no-pager flag outputs everything without waiting for a pager. Add --since "1 hour ago" to narrow the window. Add -f to follow logs in real time, equivalent to tail -f.

One of the prints in your Python script will appear in the journal as part of the service output. This is why writing to stdout in your scripts is the right way to handle logging when running under systemd — journalctl picks it up automatically.

Troubleshooting a failed service

If the service fails to start, systemctl status shows the error and the last few log lines. Common causes:

  • Wrong path in ExecStart — always use the full path to the executable. /usr/bin/python3 not python3.
  • Port already in use — another process is already bound to the port. Check with ss -tlnp | grep 8088.
  • Permission denied — the User specified in the service file does not have access to the script or its working directory.
  • Missing dependency — the service starts before something it needs. Check the After= line.

After fixing the service file, always run sudo systemctl daemon-reload before restarting, or systemd will still be running the old configuration.

The commands you will use daily

sudo systemctl start servicename     # Start now
sudo systemctl stop servicename      # Stop now
sudo systemctl restart servicename   # Stop and start
sudo systemctl reload servicename    # Reload config without stopping
sudo systemctl status servicename    # Check state and recent logs
sudo systemctl enable servicename    # Start at boot
sudo systemctl disable servicename   # Stop starting at boot
sudo systemctl daemon-reload         # Reload unit files after editing
sudo journalctl -u servicename -f   # Follow logs in real time
Reload vs restart: reload sends the running process a signal to reread its config without stopping. restart stops and starts the process. If your service does not support config reload, reload will not do anything — you need restart.

Related guides

Account services are unavailable until the production backend is configured.