GICT knowledge base
What Is a systemd Service and How to Create One
Published
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/python3notpython3. - 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 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
Plan a Safe Redis-to-Valkey Migration
Plan a Redis-to-Valkey migration: check version compatibility, rehearse recovery, stop writers for cutover and protect against lost writes during rollback.
Linux AdministrationHow to Restrict an SSH Key to One Command
Restrict an SSH key to one command with authorized_keys options. Protect the command and check that unwanted shell and forwarding access are denied.
Linux AdministrationHow to Schedule Tasks with systemd Timers
Schedule tasks with systemd timers. Verify service results separately from timer status, test missed-run handling and use an explicit UTC schedule.
Linux AdministrationHow to Restrict a Linux Service with systemd Security Settings
Restrict a Linux service with systemd drop-ins, a dedicated account, read-only paths, private temporary storage, and controlled writable directories.
