HostiServer
2026-06-27 13:01
What is systemctl: A Practical Guide to systemd in 2026
What is systemctl: a practical guide to systemd in 2026
A sysadmin in 2026 spends 90% of their time interacting with a server through a single command: systemctl. Starting a web server, restarting a database after a botched update, diagnosing why Nginx didn't come up in the morning, setting up a custom service. It all goes through one utility.
But systemctl is only the frontend to systemd: the initialization and service management system behind everything that runs on modern Linux. Understanding how it works under the hood is what separates the engineer who copies commands from StackOverflow from the one who can diagnose a problem in five minutes or write a custom unit file for their own application.
This article is a practical guide for sysadmins and DevOps. All examples have been verified on Ubuntu 24.04 LTS / Debian 12, Rocky/AlmaLinux 9, and Arch Linux. Where behavior differs between distributions, it's called out explicitly.
ℹ️ Who this article is for: sysadmins who work with Linux servers daily; DevOps engineers writing their own unit files; developers whose application needs its own service unit.
1. What an init system is
1.1 The role of init in Linux
When Linux boots, the kernel does the minimum: initializes drivers, mounts the root partition, launches the first user-space process. That first process has PID 1 and is called init. Everything else (network, file systems, services, the graphical interface) is started by it.
What init does:
- Bring up the rest of the system in the right order (network → databases → web server).
- Stay the "parent" of all processes: if a process becomes an orphan, PID 1 inherits it.
- Reap zombie children: process exit codes have to be read.
- React to signals (SIGTERM on shutdown, and so on).
- Control the service lifecycle: restart on crash, stop on shutdown.
1.2 Alternatives to systemd
Before systemd (2010) the standard was SysVinit, a minimalist init from 1983. The logic: shell scripts in /etc/init.d/ that ran sequentially, with no parallelism, no dependencies. In 2026 it's an exotic choice, but it's worth understanding conceptually.
Beyond systemd and SysVinit, a few live alternatives remain for those who deliberately pick minimalism: OpenRC (default in Gentoo, an option in Alpine and Artix) and runit (Void Linux, ~5000 lines of code). In practice, 95% of servers in 2026 run systemd. The remaining 5% are either historical distributions (Devuan) or a conscious vote for minimalism.
1.3 When systemd isn't the best choice
Systemd is built around the assumption that you have a full Linux server with adequate hardware resources. That isn't always justified:
- Embedded and IoT. On a Raspberry Pi Zero W or an OpenWrt router every megabyte of RAM counts. Systemd eats 30-80 MB of resident memory altogether. BusyBox-based systems use busybox init (~50 KB).
- Containers. Docker doesn't run init at all: a single declared command runs as PID 1. For a "full" container, tini or dumb-init are typically used.
- Minimal images. If you're rolling out hundreds of micro-VMs on a single hypervisor, Alpine Linux on musl + OpenRC gives a base image of ~5 MB; systemd + glibc can't be squeezed below 60-80 MB.
⚠️ What NOT to do: tear systemd out of a regular server "because someone on Reddit said so." Over 10+ years the architecture has matured considerably. For a typical VPS, dedicated server, or corporate Linux environment, systemd remains the optimal choice.
2. systemd overview
2.1 Why systemd became the standard
Systemd was developed by Lennart Poettering at Red Hat in 2010. The current maintainer (since 2024) is Luca Boccassi at Microsoft. Why systemd won the market despite harsh criticism:
- Parallel startup. SysV started services sequentially: Apache waited on Bind, Bind on NetworkManager. On modern hardware this added 20-60 seconds to boot. Systemd uses parallel startup, and boot shortens by a factor of 2-3.
- Declarative unit files. Instead of a 200-line bash script you get a short INI file with clear sections. Less room for bugs.
- A unified ecosystem. Logs (journald), networking (networkd), timers in place of cron, mounting: all under one umbrella.
- Major distros adopted it. Fedora 15 (2011), Arch (2012), Debian 8 and Ubuntu 15.04 (2015), RHEL 7 (2014). Once the enterprise distros moved, holding out became pointless for the rest.
2.2 Units and their types
The basic unit in systemd is, well, a unit: a declarative description of what the system needs to do. The type is determined by the file extension.
| Extension | Type | What it's for |
|---|---|---|
.service |
Service | Starting and managing a daemon |
.timer |
Timer | Periodic execution (cron replacement) |
.socket |
Socket | IPC or network socket |
.mount |
Mount | File system mounting |
.target |
Target | A group of other units (runlevels replacement) |
.path |
Path | Start on file/directory change |
In practice, 90% of an admin's time goes to .service and .timer. The rest is either generated automatically or niche.
2.3 Targets — the replacement for runlevels
SysV had the concept of runlevels — a system operating mode (3 = multi-user, 5 = graphical). In systemd they're replaced by targets, which are more flexible and use names instead of numbers.
| Target | SysV equivalent | What it means |
|---|---|---|
multi-user.target |
3 | Multi-user, no GUI (the standard for servers) |
graphical.target |
5 | Multi-user + GUI |
rescue.target |
1 | Single-user, minimal services |
emergency.target |
— | Emergency mode (only a root shell) |
Check the current default target:
# Current default target
systemctl get-default
# multi-user.target
3. Unit files
3.1 File structure: [Unit], [Service], [Install]
A unit file is in INI format with three main sections. Let's look at a simplified example for Nginx:
# /usr/lib/systemd/system/nginx.service
[Unit]
Description=A high performance web server and a reverse proxy server
After=network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s QUIT $MAINPID
TimeoutStopSec=5
[Install]
WantedBy=multi-user.target
In actual Debian/Ubuntu nginx packages you'll see a more complex ExecStop that uses start-stop-daemon — that's a historical Debian-team workaround for SysVinit backward compatibility. In pure systemd style (as in the upstream from nginx.org) the stop is just a kill with $MAINPID, which is far clearer to read.
[Unit] section: metadata and dependencies
- Description — a human-readable description shown by
systemctl status. - After / Before: startup ordering.
- Wants — a soft dependency: "try to start this with me."
- Requires — a hard dependency: "I won't start without it."
[Service] section: how to start
- Type: how systemd expects the service to "have started":
simple(default),forking,oneshot,notify. - ExecStart: the main start command, full path.
- ExecReload / ExecStop: reload and stop commands.
- Restart: restart policy:
no,on-failure,always. - User / Group: which account to run as (important for security).
[Install] section: how to enable at boot
- WantedBy: the target to hook into on
systemctl enable(usuallymulti-user.target).
One of the most common bugs in custom unit files: Type=simple on a service that performs a classic fork() and exits. systemd sees that the main process has "died" and starts restarting it, while the real daemon happily runs in the forked child. For those cases you need Type=forking.
And if your daemon is modern (Go, Rust, or Python with sd_notify() support), use Type=notify. The service then tells systemd directly "I've not only started, I've already connected to the database, warmed up the cache, and I'm ready to take traffic," and dependent services won't fire prematurely. PostgreSQL, MariaDB, sshd, Nginx (with the notify patch) support this out of the box, and in 2026 it's the de facto standard for any serious backend service.
3.2 Where unit files live and how override works
| Path | Purpose |
|---|---|
/etc/systemd/system/ |
Local (admin-owned) unit files. Highest priority. |
/usr/lib/systemd/system/ |
Units shipped by distribution packages (the canonical location on all modern distros after UsrMerge). |
On older Debian/Ubuntu you might have seen the path /lib/systemd/system/. On modern distros (Debian 12, Ubuntu 22.04+, all RHEL-compatibles, Arch), after the UsrMerge process, /lib became a symlink to /usr/lib, so units effectively live at one canonical path.
If nginx.service sits in both places, systemd picks the one from /etc/systemd/system/. That makes it possible to override package-shipped units without editing files that would be overwritten on the next package update.
Override via drop-in directories
Instead of copying the entire unit file, use a drop-in:
sudo systemctl edit nginx.service
The command creates /etc/systemd/system/nginx.service.d/override.conf, where you can add only what you want to override:
[Service]
LimitNOFILE=65536
Environment="NGINX_WORKER_PROCESSES=auto"
The original unit file stays untouched, the override survives package upgrades, and at a glance you can see "what exactly was changed."
3.3 Writing your own unit file
A realistic example: a Go application at /usr/local/bin/myapi listening on port 8080. It needs to run as a service with autostart, resource limits, and restart on failure.
Create /etc/systemd/system/myapi.service:
[Unit]
Description=My API server (Go application)
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=myapi
Group=myapi
WorkingDirectory=/opt/myapi
Environment="LOG_LEVEL=info"
EnvironmentFile=/etc/myapi/env
ExecStart=/usr/local/bin/myapi --config /etc/myapi/config.yaml
Restart=on-failure
RestartSec=5s
StartLimitBurst=3
StartLimitIntervalSec=60
# Security hardening options
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/log/myapi /var/lib/myapi
# Resource limits via cgroups v2
LimitNOFILE=65536
MemoryMax=512M
CPUQuota=200%
[Install]
WantedBy=multi-user.target
Breaking it down by block:
- Dependencies. We start after the network and PostgreSQL.
Requires=postgresql.servicemeans that if PostgreSQL doesn't start, neither will our service. - User/Group. We do NOT run as root (important for security). Create the user first:
sudo useradd -r -s /usr/sbin/nologin myapi. - Restart=on-failure with limits: 3 attempts within 60 seconds, otherwise stop (no infinite loop).
- Hardening.
NoNewPrivileges,PrivateTmp,ProtectSystem=strict,ProtectHomeminimize permissions.ReadWritePathsexplicitly allows writes only into the specified directories. - Resource limits. Through cgroups v2 we cap memory and CPU.
CPUQuota=200%= 2 full cores.
Applying changes
sudo systemctl daemon-reload
sudo systemctl enable --now myapi.service
--now simultaneously enables the service (enable) and starts it (start).
The most common reason for "I edited the unit file and the changes don't apply" is forgetting systemctl daemon-reload. Systemd picks up changes only when you tell it to. The exception is systemctl edit, which runs daemon-reload automatically.
4. systemctl — managing services
Everything above was about unit files — the declarative description of a service. systemctl is the tool you use to manage those services. It accounts for ~90% of an admin's daily systemd work.
4.1 Core commands
| Command | What it does |
|---|---|
systemctl start <unit> |
Start the service now |
systemctl stop <unit> |
Stop the service |
systemctl restart <unit> |
Full restart (stop + start) |
systemctl reload <unit> |
Soft config reread (without stopping the process) |
systemctl enable --now <unit> |
Enable autostart + start immediately |
systemctl disable --now <unit> |
Disable autostart + stop |
systemctl mask <unit> |
Completely block startup (symlink to /dev/null) |
systemctl daemon-reload |
Reread unit files from disk |
ℹ️ The difference between disable and mask: disable removes the service from autostart but it can still be started manually. mask blocks it entirely, so even systemctl start will return an error. Useful when you need to guarantee that a service will never start (for example, masking apache2 on an nginx server).
4.2 Status views and listings
The workhorse for diagnostics:
systemctl status nginx.service
It shows: whether the service is active, since when, the main process PID, the cgroup tree with all child processes, and the last 10 log lines from journald. This is the most useful "one-shot" diagnostic report.
Useful listings:
# All active units
systemctl list-units
# Active services only
systemctl list-units --type=service --state=active
# Failed services
systemctl --failed
# All unit files (enable/disable state)
systemctl list-unit-files --type=service
The difference: list-units shows what's currently loaded in memory (the actual state); list-unit-files shows what's on disk as files (the declarative state).
5. journalctl — working with logs
systemd-journald is the centralized logger. Every service launched through systemd automatically writes its stdout/stderr into the journal. So even if your application doesn't write any logs on its own, whatever it outputs to the console lands in journald.
5.1 Basic syntax
# All logs for all time
journalctl
# In reverse order (newest at the top)
journalctl -r
# Last N lines
journalctl -n 100
# Streaming (like tail -f)
journalctl -f
# Only a specific unit
journalctl -u nginx.service
# Multiple units at once
journalctl -u nginx.service -u php8.4-fpm.service
ℹ️ A PHP-FPM example: if you have a PHP site and 502 Bad Gateway errors crop up, start with journalctl -u php8.4-fpm -u nginx --since "10 min ago". That shows the logs of both services side by side on a single timeline, and you can immediately see whether PHP-FPM was crashing or Nginx couldn't reach it. For tuning PHP-FPM itself (pool tuning, OPcache, JIT), see our article on PHP server optimization.
5.2 Filtering
# By time
journalctl --since "14:00"
journalctl --since "1 hour ago"
journalctl --since "2026-06-15 09:00" --until "2026-06-15 10:00"
journalctl --since today
journalctl --since yesterday
# By priority (syslog levels)
# Shows this level and above
journalctl -p err # err, crit, alert, emerg
journalctl -p warning # warning and above
# Kernel messages only (like dmesg)
journalctl -k
# Everything that happened in the current boot
journalctl -b
# Everything from the previous boot (-1 = previous)
journalctl -b -1
# List of all boots
journalctl --list-boots
5.3 Managing the journal
On modern distributions (Ubuntu 18.04+, Debian 12+, RHEL 8+) logs are persistent: they live in /var/log/journal/ and survive reboots. On older ones they may be volatile (in RAM, cleared on reboot).
Check the current log size:
journalctl --disk-usage
# Archived and active journals take up 384.0M in the file system.
Clean up old logs:
sudo journalctl --vacuum-time=7d # drop everything older than 7 days
sudo journalctl --vacuum-size=500M # keep at most 500 MB
In practice the combo journalctl -u <unit> -f --since "10 min ago" covers most diagnostic tasks: it shows the context from the last 10 minutes and immediately streams new events. If you keep it in muscle memory as "the default command," most questions about a service get an answer right away, without digging through manuals.
6. systemd-analyze — boot diagnostics
systemd-analyze lets you see what took time during boot, and how much of it. Without it, optimizing boot is reading tea leaves: you can speed up a service that wasn't blocking the start in the first place, and leave the real culprit untouched.
6.1 Total boot time
systemd-analyze time
Sample output:
Startup finished in 4.231s (kernel) + 12.456s (userspace) = 16.687s
multi-user.target reached after 12.234s in userspace.
- kernel: time from BIOS to PID 1 starting.
- userspace: time from PID 1 to reaching the default target. This is what we can optimize.
6.2 blame: who's slowing boot down
systemd-analyze blame
Prints a list of units sorted by their startup time:
8.234s plymouth-quit-wait.service
3.456s NetworkManager-wait-online.service
2.123s docker.service
1.892s snapd.service
876ms postgresql@16-main.service
This is not the time the service contributed to boot but its absolute startup time. Some services start in parallel and don't block boot. For a more accurate picture there's critical-chain.
6.3 critical-chain: the actual bottleneck
systemd-analyze critical-chain
Shows the critical dependency chain — the chain of services that actually determines when boot finishes:
multi-user.target @12.234s
└─docker.service @9.876s +2.123s
└─network-online.target @9.834s
└─NetworkManager-wait-online.service @6.378s +3.456s
└─NetworkManager.service @5.234s
Here you can see it: docker is waiting for network-online, which is waiting for NetworkManager-wait-online (3.4 seconds!). If we want to speed up boot, we start with NetworkManager-wait-online.
6.4 verify: validating unit files
systemd-analyze verify /etc/systemd/system/myapi.service
A quick linter for unit files. It flags syntax errors, missing dependencies, and deprecated options. Useful to run before systemctl daemon-reload. If everything is fine, the command outputs nothing.
6.5 dot: visualizing dependencies
The systemd-analyze dot command generates a dependency graph description in Graphviz DOT format, which you then render as SVG via the dot utility (the graphviz package). Handy when you need to see at a glance what depends on what:
systemd-analyze dot nginx.service | dot -Tsvg > nginx-deps.svg
Install graphviz: sudo apt install graphviz on Debian/Ubuntu, sudo dnf install graphviz on Rocky/Alma. The result is a directed graph showing Requires, After, and Wants relationships between services. Especially helpful when you inherit an unfamiliar installation and need to figure out exactly how the custom services interweave.
7. Other utilities in the systemd ecosystem
systemd isn't just systemctl. The suite includes separate utilities that replace older standalone programs. The most useful day-to-day:
hostnamectl
hostnamectl # current state
sudo hostnamectl set-hostname web01.example.com
Replaces editing /etc/hostname but does it properly, updating all the places the name is stored.
timedatectl
timedatectl # current state
sudo timedatectl set-timezone Europe/Kyiv
sudo timedatectl set-ntp true # enable NTP sync
Controls the system clock, time zone, and NTP setup. Fine-grained synchronization parameters (NTP servers, intervals) are set in /etc/systemd/timesyncd.conf.
loginctl
loginctl # list active sessions
loginctl session-status 5 # session details
sudo loginctl kill-user 1000 # terminate a user's session
The remaining utilities (localectl, networkctl, resolvectl, machinectl) are in man systemd. If they're relevant to your particular scenario, you'll learn about them from context.
8. Summary
Systemctl, journalctl, and systemd-analyze are the minimum set that covers the vast majority of a sysadmin's daily work in 2026. Everything stands on these three utilities: from starting and restarting services to diagnosing why something isn't working and optimizing the boot process.
What to carry from this article into daily practice:
- Drop-in directories instead of copying unit files.
systemctl edit nginxsurvives package updates; a copy of a unit file in/etc/systemd/system/does not. - The right
Typefrom the start. Knowing the difference betweensimple,forking, andnotify, you'll write a working unit file in five minutes instead of three hours of guessing why the service keeps restarting. - journalctl with filters.
-u,-p,--since,-f: four flags that cover most diagnostic scenarios. Especially in combination with a specific unit. - systemd-analyze blame and critical-chain. Before buying a faster SSD "because the server takes forever to boot," look at what's actually slowing it down. Often it's NetworkManager-wait-online or something similar that can be disabled.
- Hardening in your own unit files.
NoNewPrivileges,PrivateTmp,ProtectSystem,User=without root: basic hygiene that takes five minutes and prevents most compromise scenarios. - Resource limits via cgroups v2.
MemoryMax,CPUQuota,TasksMaxin the unit file: a single process won't eat all the server's RAM even if it has a memory leak.
The rest of the fine points (user-level systemd, socket activation, systemd-nspawn, systemd-homed) are separate big topics that become relevant once you've nailed the basics. Return to them when an actual task appears, not "just in case" — otherwise you spend time studying things you'll never use at work.
🚀 Linux servers with full root access from Hostiserver
Everything covered in this article is work on "your own" Linux server with full control over systemd, unit files, and resources. Hostiserver provides exactly that kind of server: a KVM-isolated VPS with root access, or a full bare-metal dedicated.
💻 Cloud (VPS) Hosting
- From $19.95/mo — KVM isolation, dedicated vCPU and RAM
- Ubuntu 24.04 / Debian 12 / Rocky Linux 9 out of the box, up-to-date systemd
- Full root: write your own unit files, configure journald, cgroups
- API access for automation, Terraform and Ansible compatible
- 24/7 DevOps support: engineers will help untangle systemd problems
🖥️ Dedicated Servers
- From $90/mo — a physical server with full hardware control
- Any distribution at install time: Ubuntu, Debian, RHEL-compatibles, Arch
- Free migration with engineer assistance
- 99.9% uptime SLA guaranteed in the contract
💬 Not sure which option fits you?
💬 Drop us a line and we'll help you figure it out!
Frequently asked questions
- Why does my service show "active (running)" but actually doesn't work?
The usual cause:
Type=simpleon a service that itself does a fork(). systemd sees the main process "ran ExecStart" and immediately considers it active. But then the main process forks a child and exits. Check the service's behavior: if it exits right after start but launches a child, that'sType=forking. If it's a modern service with sd_notify support, useType=notify. Another option: the real process crashed but the PID file got stuck. Check:systemctl statuswill show the Main PID — cross-check it withps aux | grep <pid>.
- How do I make a service restart on any crash?
In the [Service] section add
Restart=alwaysandRestartSec=5s(a pause before restart). But always set a cap on the number of attempts:StartLimitBurst=5andStartLimitIntervalSec=60(5 attempts in 60 seconds, then systemd stops the service). Without that, a misconfigured service will spin forever, eating logs and CPU. To find the cause of the restart, checkjournalctl -u <unit>around the moment of the crash.
- How do I find which unit file systemd actually loaded?
systemctl cat <unit>shows the exact file systemd is using, together with all drop-in overrides. It's the fastest way to confirm that your override from/etc/systemd/system/<unit>.d/override.confreally applied. Another useful command:systemctl show <unit>, which shows the effective values of all parameters as systemd "sees" them after parsing all the files.
- What should I do if a service shows "failed" at boot but starts fine afterwards?
Most likely a dependency problem at boot. The service depends on something that isn't ready yet (database, network, mount) and starts too early. Step one:
journalctl -u <unit> -bshows the logs of that specific failed start. Step two: add the rightAfter=in the [Unit] section. For the network it's oftenAfter=network-online.target+Wants=network-online.target(both:Aftersets the order,Wantsactually makes network-online get loaded).
- How do I limit memory for a single service?
Through cgroups v2, which systemd uses out of the box. In the [Service] section add
MemoryMax=512M(a hard limit, kill on excess) orMemoryHigh=400M(a soft one, throttling). The same for CPU:CPUQuota=50%caps the process at half a core,CPUQuota=200%gives it two full cores. To check current usage:systemctl status <unit>displays Memory and CPU rows. For analytics there'ssystemd-cgtop, which shows top by cgroups.
- Is it safe to disable systemd-resolved?
It depends on the distro. On Ubuntu 18.04+ and Debian 12+ resolved is integrated via
/etc/resolv.conf, which is a symlink to/run/systemd/resolve/stub-resolv.conf(with the local stubnameserver 127.0.0.53in it). Disabling it without an alternative will break DNS. If you want a regular/etc/resolv.confwith DNS servers set by hand: 1) disable resolved (systemctl disable --now systemd-resolved); 2) remove the symlink (sudo rm /etc/resolv.conf); 3) create a regular file withnameserverlines (for example,nameserver 1.1.1.1). On RHEL/Rocky resolved isn't used by default — NetworkManager handles DNS via its plugin — so disabling resolved breaks nothing.
- Can I check the security of my own unit file automatically?
Yes, with
systemd-analyze security <unit>. It produces a score from 0.0 (bulletproof) to 10.0 (UNSAFE) and explains which hardening options are missing:NoNewPrivileges,ProtectSystem,PrivateTmp,ProtectHome, and so on. Worth running for all your own unit files — you'll get a prioritized list of what to improve with minimal effort.