Skip to content

Reading and Writing Unit Files — Systemd Services


Every service you’ve controlled with systemctl so far is defined by a unit file — a plain text configuration file, readable and editable with tools you already know. This chapter covers that structure directly: what’s genuinely required, what’s optional and situational, and how to build one up from a bare minimum working service to something production-ready.

Reading An Existing Unit File

Start by looking at one that already exists:

cat /lib/systemd/system/ssh.service

Note

If you don’t know the exact location of the service, you can:

systemctl cat ssh

Both produces the same output.

[Unit]
Description=OpenBSD Secure Shell server
After=network.target auditd.service
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Service]
EnvironmentFile=-/etc/default/ssh
ExecStartPre=/usr/sbin/sshd -t
ExecStart=/usr/sbin/sshd -D $SSHD_OPTS
ExecReload=/usr/sbin/sshd -t
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
Restart=on-failure
RestartPreventExitStatus=255
Type=notify
RuntimeDirectoryPreserve=yes

[Install]
WantedBy=multi-user.target
Alias=sshd.service

Three sections, each with a specific job. [Unit] describes general metadata and ordering relative to other units. [Service] — only present on .service units — describes how to actually run the program. [Install] describes how this unit hooks into the enable/disable mechanism from the last chapter.

What’s Actually Required

This is the part worth being precise about, since most unit file examples show every possible option at once without clarifying which ones you can genuinely skip.

In [Service], only one key is truly mandatory: ExecStart=. This is the actual command systemd runs to start your service — without it, systemd has nothing to execute at all, and the unit will fail to start.

The [Install] section is entirely optional — but only if you never intend to enable this unit. Recall from the last chapter that enable works by creating a symlink into a target’s dependency directory. Without a WantedBy= line telling systemd which target should depend on this unit, there’s nothing for enable to link — running systemctl enable on a unit with no [Install] section will fail outright, even though systemctl start on the same unit works perfectly well.

Everything else is genuinely optional, filling in behavior you’d otherwise get systemd’s defaults for — Type=simple is assumed if you don’t specify Type= at all, for instance.

The Bare Minimum: A Working Service

Create the simplest possible unit file that actually runs something:

sudo nano /etc/systemd/system/hello.service
[Service]
ExecStart=/usr/bin/sleep infinity

That’s genuinely it — no [Unit], no [Install], just one line telling systemd what to run. Load and start it:

sudo systemctl daemon-reload
sudo systemctl start hello
systemctl status hello

You’ll see it running, tracked exactly like any other service, with a real Main PID. Try enabling it, though:

sudo systemctl enable hello

This fails, or does nothing useful — exactly the consequence of skipping [Install] described above. Clean it up:

sudo systemctl stop hello
sudo rm /etc/systemd/system/hello.service
sudo systemctl daemon-reload

Building It Up: A Realistic Unit File

Starting from that bare minimum, here’s what a genuinely production-ready unit file looks like, with each addition explained:

[Unit]
Description=My Example Backend Service
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/my-backend-service
Restart=on-failure
User=myservice
WorkingDirectory=/opt/my-backend
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target
  • Description= — not required, but genuinely important in practice: it’s what shows up in systemctl status and list-units, making the service identifiable at a glance instead of just a bare filename.
  • After=network.target — an ordering hint, not a hard dependency (recall the After=/Requires= distinction from the Units & Targets chapter) — ensures this service doesn’t attempt to start before basic networking is available, appropriate for anything that needs network access to function.
  • Type=simple — the default anyway, but stating it explicitly documents your intent clearly for the next person reading the file.
  • Restart=on-failure — without this, a crashed service simply stays dead until someone manually restarts it. This tells systemd to restart it automatically if it exits with a failure status, which is what you want for essentially any long-running service.
  • User=myservice — runs the service as a dedicated, unprivileged system account rather than root, directly applying the least-privilege principle from the Users & Groups section — you’d create this account first with useradd -r, exactly as covered there.
  • WorkingDirectory= — sets the directory the process runs from, avoiding ambiguity about relative paths the program might use internally.
  • Environment= — sets an environment variable available to the running process, useful for configuration that shouldn’t be hardcoded into the program itself.
  • WantedBy=multi-user.target — the piece that makes enable actually work, registering this service as something multi-user.target should bring up automatically.

Best Practices

Always use absolute paths in ExecStart=. Unlike your interactive shell, a systemd service doesn’t inherit your PATH the way a normal terminal session does — ExecStart=myprogram will typically fail to find myprogram at all, while ExecStart=/usr/local/bin/myprogram works reliably regardless of environment.

Avoid shell syntax directly in ExecStart= unless you mean it. ExecStart= runs the given command directly, not through a shell — pipes, redirection, and variable expansion won’t work the way they would in a script. If you genuinely need shell features, wrap the command explicitly: ExecStart=/bin/bash -c '/usr/local/bin/myprogram > /var/log/myprogram.log'.

Run as a dedicated, unprivileged user whenever the service doesn’t need root. Defaulting to root because it’s the path of least resistance is exactly the kind of shortcut the Permissions and Users & Groups sections warned against — a compromised service running as its own limited account can only do what that account can do.

Set Restart=on-failure for anything meant to run continuously. The default is no automatic restart at all, which is rarely what you actually want for a long-running service.

Place custom unit files in /etc/systemd/system, never in /lib/systemd/system. That distinction was covered in the Units & Targets chapter for a reason — /lib is package-managed territory, and a future package update could silently overwrite anything you’d placed there directly.

Always run daemon-reload after any edit, and validate the file’s syntax before relying on it:

sudo systemd-analyze verify /etc/systemd/system/my-backend.service

This catches structural mistakes — a misspelled directive, a missing required value — before you find out the hard way with a service that silently refuses to start.

What’s Next

You can now read, write, and validate unit files confidently, from a bare minimum through to something genuinely production-ready. Everything covered so far has assumed the system already booted successfully. The next chapter steps back to the very beginning — the full sequence a Linux system goes through from power-on to a running systemd.

Last updated on