Why systemd?
Every Linux machine that matters runs systemd. It’s the init system — the first process that boots, the parent of all other processes, and the thing responsible for starting, stopping, and supervising services. If you’ve ever run sudo systemctl restart nginx, you’ve already used it. But most people only scratch the surface.
In this tutorial, you’ll learn how to manage services with systemd like a pro: starting, stopping, enabling, debugging, and even writing your own service files so your applications stay alive after crashes and reboots.
Prerequisites
- Any modern Linux distribution (Ubuntu 18.04+, Debian 10+, Fedora, CentOS 7+, Arch, etc.)
- A terminal and sudo access
- Basic familiarity with the command line
The Essentials: systemctl
systemctl is your primary tool for interacting with systemd. Think of it as the remote control for every service on your machine.
Check the status of a service
sudo systemctl status nginx
This tells you whether the service is running, when it started, recent log lines, and whether it’s set to start on boot. It’s the first thing you should run when something isn’t working.
Start and stop services
# Start a service immediately
sudo systemctl start nginx
# Stop a service
sudo systemctl stop nginx
# Restart a service (stop + start)
sudo systemctl restart nginx
# Reload configuration without restarting (zero-downtime)
sudo systemctl reload nginx
# Reload-or-restart (graceful fallback)
sudo systemctl reload-or-restart nginx
Why reload vs restart? Reload tells the running process to re-read its configuration file without killing it. If the service supports it, this means no downtime. Nginx, Apache, and PostgreSQL all support graceful reloads. When in doubt, use reload-or-restart.
Enable and disable services (boot behavior)
# Start service on boot (enable a symlink)
sudo systemctl enable nginx
# Disable auto-start on boot
sudo systemctl disable nginx
# Do both in one step: start now AND enable on boot
sudo systemctl enable --now nginx
What “enable” actually does: It creates a symlink from the service file in /lib/systemd/system/ to a directory called a “want” (like multi-user.target.wants/). Nothing more. It doesn’t start the service — it just tells systemd “when you reach this boot target, start me too.”
See all running services
# List all active (running) units
systemctl list-units --type=service --state=running
# List all loaded service units (running, stopped, or failed)
systemctl list-units --type=service
# List all installed service files (enabled/disabled)
systemctl list-unit-files --type=service
Reading Unit Files
Every systemd service is defined in a unit file, usually found at /lib/systemd/system/<service>.service (for system packages) or /etc/systemd/system/<service>.service (for your custom overrides).
Let’s look at the Nginx unit file:
systemctl cat nginx
You’ll see something like this:
[Unit]
Description=A high performance web server and a reverse proxy server
After=network.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload
PrivateTmp=true
[Install]
WantedBy=multi-user.target
Three sections matter here:
- [Unit] — metadata and dependencies (
After=network.targetmeans “don’t start until networking is up”) - [Service] — how to start, stop, and supervise the process
- [Install] — when to start (which “target” to attach to)
Writing Your Own Service File
This is where systemd gets powerful. Got a Python script, a Node.js app, or a bot that should always be running? Write a service file for it.
Example: A simple Python bot
Say you have a Python script at /home/noodly/bot/main.py and you want it to start automatically after boot, restart if it crashes, and only run after networking is available.
Create a new unit file:
sudo nano /etc/systemd/system/mybot.service
Paste in:
[Unit]
Description=My Python Bot
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=noodly
WorkingDirectory=/home/noodly/bot
ExecStart=/usr/bin/python3 /home/noodly/bot/main.py
Restart=on-failure
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Let’s break down the key options:
| Option | What it does |
|---|---|
Type=simple |
The process runs in the foreground. systemd considers it “started” as soon as the process is running. Use this for most apps. |
Type=forking |
The process forks (daemonizes) itself. systemd watches for the PID file. Use for nginx, apache, etc. |
User=noodly |
Run as this user, not root. Always use this for security. |
Restart=on-failure |
Restart if the process exits with a non-zero code. |
RestartSec=10 |
Wait 10 seconds before restarting (prevents rapid restart loops). |
After=network-online.target |
Don’t start until the network is actually connected (not just the network service started). |
After creating the file, reload systemd’s configuration and start the service:
# Reload systemd to pick up the new unit file
sudo systemctl daemon-reload
# Start the service now
sudo systemctl start mybot
# Enable it for boot
sudo systemctl enable --now mybot
# Check it's running
sudo systemctl status mybot
Debugging with journalctl
When a service fails, journalctl is your best friend. It reads the structured logs that systemd collects from every service.
# View all logs for a specific service
sudo journalctl -u mybot
# Follow logs in real-time (like tail -f)
sudo journalctl -u mybot -f
# Show only logs since last boot
sudo journalctl -u mybot -b
# Show logs from the last 30 minutes
sudo journalctl -u mybot --since '30 min ago'
# Show logs between two timestamps
sudo journalctl -u mybot --since '2026-06-14 09:00' --until '2026-06-14 10:00'
# Show only error-level and above
sudo journalctl -u mybot -p err
Pro tip: Always check journalctl -u <service> before anything else when debugging. It almost always tells you exactly what went wrong — missing config file, permission denied, port already in use, etc.
Masking and Overriding Services
Masking (permanently disable)
If you want to make absolutely sure a service can never be started — even by accident or by another service’s dependency:
# Mask a service (creates a symlink to /dev/null)
sudo systemctl mask servicename
# Unmask it later
sudo systemctl unmask servicename
Example: you don’t need a Bluetooth daemon on a headless server:
sudo systemctl mask bluetooth.service
Overriding without modifying the original
Package updates will overwrite unit files in /lib/systemd/system/. To make persistent customizations, use a drop-in:
# Create an override directory
sudo systemctl edit nginx
This opens an editor with a file at /etc/systemd/system/nginx.service.d/override.conf. Add only the sections you want to change:
[Service]
Restart=always
RestartSec=5
Save, then sudo systemctl daemon-reload and you’re done. The original file stays untouched.
Timers: cron’s Modern Replacement
systemd timers can replace cron jobs. They offer better logging, dependency management, and retry logic.
Create /etc/systemd/system/backup.service:
[Unit]
Description=Daily backup script
[Service]
Type=oneshot
ExecStart=/home/noodly/scripts/backup.sh
User=noodly
Create /etc/systemd/system/backup.timer:
[Unit]
Description=Run backup daily at 2am
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true means: if the machine was off at 2am, run the backup as soon as it boots. Enable the timer (not the service):
sudo systemctl enable --now backup.timer
# List all active timers
systemctl list-timers
Common Pitfalls
- Forgot
daemon-reload: After editing any unit file, always runsudo systemctl daemon-reloadbefore restarting. systemd won’t see your changes otherwise. - Using
Type=forkingfor a foreground app: If your app doesn’t daemonize, useType=simple. systemd will think the service failed because it expects a fork. - Running as root unnecessarily: Always set
User=andGroup=unless your service genuinely needs root privileges. - No
ExecStopneeded for Type=simple: systemd sends SIGTERM by default when stopping. UseExecStoponly if you need custom shutdown behavior. - Chaining too many
After=dependencies: Keep dependencies minimal. UseAfter=network-online.targetinstead of listing every service individually — one target should cover it.
Quick Reference Card
# Management
systemctl start|stop|restart|reload|status <service>
systemctl enable|disable|mask|unmask <service>
systemctl enable --now <service> # start + enable
# Information
systemctl is-active <service> # exit code 0 if running
systemctl is-enabled <service> # exit code 0 if enabled
systemctl is-failed <service> # exit code 0 if failed
systemctl show <service> --property=MainPID
# Logs
journalctl -u <service> -f # follow
journalctl -u <service> -p err # errors only
journalctl -u <service> --since today
# Timers
systemctl list-timers # all scheduled tasks
systemctl start <timer>.timer # trigger a timer
Wrapping Up
systemd isn’t glamorous, but it’s the backbone of every modern Linux system. Once you can write a service file and read the logs, you stop worrying about “what happens when the server restarts” and start treating crashes as a non-event. That’s a big deal.
Start with one service — a bot, a script, anything — and get comfortable with the cycle: write unit file → daemon-reload → start → enable → check status → read logs. Once that clicks, you’ll reach for systemd instinctively.
Your services deserve to stay alive. systemd will keep them that way.
Leave a Reply