The most surprising thing about systemd unit files is that they’re not just for starting services; they’re a declarative way to define any kind of system resource and its dependencies.

Let’s look at a simple service that just prints "Hello, world!" to the console every 5 seconds.

[Unit]
Description=My simple hello world service

[Service]
ExecStart=/bin/bash -c "while true; do echo 'Hello, world!' >> /tmp/hello.log; sleep 5; done"
Restart=on-failure

[Install]
WantedBy=multi-user.target

If you save this as /etc/systemd/system/hello.service and then run:

sudo systemctl daemon-reload
sudo systemctl enable hello.service
sudo systemctl start hello.service

You can then check its status:

systemctl status hello.service

And see the output in /tmp/hello.log:

tail /tmp/hello.log

This gives you a tangible example of a service running under systemd’s control.

The [Unit] section is all about metadata and dependencies. Description is self-explanatory. Requires=, Wants=, Before=, and After= are how you define relationships between units. Requires= means if the required unit fails, this unit will also be stopped. Wants= is a weaker dependency; if the wanted unit fails, this one keeps running. After= and Before= control the order of activation. For example, After=network.target ensures your service starts only after the network is up.

The [Service] section defines how the service actually runs. ExecStart= is the command that launches your service. Type= can be simple (default, assumes the ExecStart process is the main process), forking (for traditional daemons that fork and exit), oneshot (for tasks that run once and exit), or notify (for services that signal systemd when they are ready). Restart= controls what happens when the service exits. on-failure is common for services that should be resilient. User= and Group= are crucial for security, defining under which user/group the service should run. WorkingDirectory= sets the current directory for the ExecStart command.

The [Install] section is used by systemctl enable and disable. WantedBy= specifies the target that should "want" this service. A target is essentially a group of units that should be active at a certain system state (e.g., multi-user.target for a normal multi-user system, graphical.target for a graphical login). When you enable a service, systemd creates a symbolic link from the target’s .wants/ directory to your service file, ensuring it gets started when that target is activated.

A common misconception is that systemd is just a replacement for SysVinit scripts. While it can run those, its true power lies in its ability to manage any kind of system resource, including mount points, devices, timers, and even simple shell commands as services. The unit file syntax is consistent across all these types. For instance, a .mount unit would have [Unit] and [Install] sections, but its [Mount] section would contain directives like What= (the device or source) and Where= (the mount point).

What most people don’t realize is how granularly you can control process execution within the [Service] section. Directives like ProtectSystem=, ProtectHome=, PrivateTmp=, and NoNewPrivileges=true can create incredibly secure, sandboxed environments for your services, significantly reducing the attack surface if a service were compromised.

When you’re debugging a service that won’t start, the first thing to check after systemctl status <service_name> is journalctl -u <service_name>. This command shows you the logs specific to that unit, often revealing the exact error message from your ExecStart command or systemd itself.

Want structured learning?

Take the full Systemd course →