Community
27
HostiServer
2026-06-27 13:01

What is systemctl: A Practical Guide to systemd in 2026

⏱️ Reading time: ~12 minutes | 📅 Updated: June 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 (usually multi-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.service means 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, ProtectHome minimize permissions. ReadWritePaths explicitly 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.

Output of systemctl status nginx — shows Active (running), Main PID, cgroup tree, and the last log lines

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.

Dependency graph of nginx.service generated with systemd-analyze dot: visualization of Requires, After, and Wants relationships

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 nginx survives package updates; a copy of a unit file in /etc/systemd/system/ does not.
  • The right Type from the start. Knowing the difference between simple, forking, and notify, 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, TasksMax in 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=simple on 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's Type=forking. If it's a modern service with sd_notify support, use Type=notify. Another option: the real process crashed but the PID file got stuck. Check: systemctl status will show the Main PID — cross-check it with ps aux | grep <pid>.

How do I make a service restart on any crash?

In the [Service] section add Restart=always and RestartSec=5s (a pause before restart). But always set a cap on the number of attempts: StartLimitBurst=5 and StartLimitIntervalSec=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, check journalctl -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.conf really 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> -b shows the logs of that specific failed start. Step two: add the right After= in the [Unit] section. For the network it's often After=network-online.target + Wants=network-online.target (both: After sets the order, Wants actually 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) or MemoryHigh=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's systemd-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 stub nameserver 127.0.0.53 in it). Disabling it without an alternative will break DNS. If you want a regular /etc/resolv.conf with 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 with nameserver lines (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.

Contents

Share this article

MANAGED VPS STARTING AT

$19 95 / mo

NEW INTEL XEON BASED SERVERS

$80 / mo

CDN STARTING AT

$0 / mo

 

By using this website you consent to the use of cookies in accordance with our privacy and cookie policy.