systemd & Service Management
systemd & Service Management
Every production Linux system you will ever operate runs systemd as PID 1 — the init system that boots the operating system, manages every service lifecycle, and is the direct parent of all other processes. Understanding systemd deeply is not optional for a DevOps engineer; it is the foundation on which you build reliable, observable services at any scale.
Units: The Building Blocks of systemd
systemd organises everything it manages into units. A unit is just a structured text file with a .service, .socket, .timer, .mount, .path, .target, or other suffix. You will spend most of your time with .service and .target units.
Unit files live in three locations — systemd merges and prioritises them in this order:
/lib/systemd/system/— shipped by packages (read-only; never edit these directly)/etc/systemd/system/— local overrides and custom units (your playground)/run/systemd/system/— transient, runtime-generated units (ephemeral)
/etc/systemd/system/ with the same name as one in /lib/systemd/system/ completely masks the package-provided file. This is how you safely override vendor defaults without touching package files — they survive package upgrades.
systemctl: The Control Interface
All day-to-day interaction with systemd goes through systemctl. Here are the commands you will use constantly in production:
systemctl enable --now rather than two separate commands. It is atomic and idempotent — running it twice is safe.
Writing a Service Unit from Scratch
Knowing how to write a correct, production-grade unit file is the most important systemd skill. Below is a real-world example for a Node.js API, annotated with the reasoning behind each directive.
After writing or editing any unit file you must reload the daemon before systemctl will see your changes:
systemctl daemon-reload after editing a unit file means systemd keeps running the old definition in memory. Your edits have zero effect, and the status output will not warn you. Make it muscle memory: edit → daemon-reload → restart/reload.
Service Types
The Type= directive tells systemd how to determine when a service has finished starting up. Choosing the wrong type causes silent race conditions at boot:
Type=simple(default) — systemd considers the service started the instantExecStartis forked. Correct only if the process never backgrounds itself and signals readiness immediately.Type=notify— process callssd_notify("READY=1")when truly ready. systemd waits for this signal. Use this for any service that takes time to initialise (database connections, cache warming). Requires the app to support it.Type=forking— legacy; for old-school daemons that double-fork. Avoid in new code.Type=oneshot— for scripts that run and exit. Combine withRemainAfterExit=yesso systemd considers it "active" after it finishes.Type=exec— likesimplebut waits for theexecve()call to succeed before considering the unit started. A safer default thansimplefor most new services.
Targets and the Boot Dependency Graph
A target is a synchronisation point — a named milestone in the boot sequence. Targets group services and establish ordering. They replace the old SysV runlevels:
The key targets to know for server workloads:
sysinit.target— filesystem mounts, kernel modules, early device setupbasic.target— sockets, timers, paths — the minimum for a working systemnetwork-online.target— network interfaces are up AND have an IP. Always use this (notnetwork.target) as a dependency for services that make outbound connections at startupmulti-user.target— the normal "server running, all services active" state; equivalent to SysV runlevel 3graphical.target— multi-user.target plus a display manager; irrelevant on headless servers
network.target is reached when the network configuration has been applied, not when the network is actually usable. A service depending only on network.target can start before DHCP assigns an IP. Always use network-online.target for services that connect to databases, message brokers, or any remote endpoint at startup.
Drop-in Overrides — The Safe Way to Customise Vendor Units
When you need to tweak a vendor-provided unit (say, increase the open-file limit for nginx) without replacing the entire file, use a drop-in. Drop-ins are surgically merged on top of the base unit by systemd:
systemctl edit nginx is the preferred workflow on any system — it creates the drop-in directory and file for you, and runs daemon-reload automatically when you save. Use it instead of manual file creation to avoid typos in paths.
Dependency Directives: Requires vs Wants
The most common source of boot-time bugs in custom unit files is misusing dependency directives. Here is the precise semantics:
Requires=— hard dependency. If the required unit fails to start, this unit is also stopped. Use sparingly.Wants=— soft dependency. systemd tries to start the wanted unit, but this unit continues even if the wanted unit fails. Preferred for most service dependencies.After=/Before=— ordering only, no dependency. Without these, systemd starts units in parallel. Always pair a dependency directive with an ordering directive.BindsTo=— likeRequires=but even stricter: if the bound unit stops for any reason (not just startup failure), this unit is also stopped immediately. Use for units that are truly meaningless without another (e.g., a service tied to a network interface).
The most common pattern for application services: Wants= + After= for soft dependencies (database, cache), and Requires= + After= only for hard infrastructure (network, required mounts).