Build a service

Write a minimal service

Create a small unit with an explicit executable, working directory, user, and restart boundary.

A good service unit answers two questions with no ambiguity. Which process should systemd run? And which account should own it? Everything else is optional. Those two are not.

Three rules for the [Service] section

Use an absolute path in ExecStart=. The service does not get your shell’s PATH, so node server.js fails while /usr/bin/node /opt/hello/server.js works.

Set WorkingDirectory= only when the program needs it. Many programs read files relative to where they started. If yours does, say so in the unit instead of hoping.

Run network applications as a dedicated unprivileged user, never as root. Create the account first:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin hello

--system gives it a low UID, nologin means nobody can log in as it. This is the account the service will use.

The unit

Let’s put it together. We are going to run a small Node.js HTTP server as the hello user, and restart it if it crashes.

Create /etc/systemd/system/hello.service:

[Unit]
Description=Practice HTTP service

[Service]
ExecStart=/usr/bin/node /opt/hello/server.js
User=hello
Restart=on-failure

[Install]
WantedBy=multi-user.target

Run systemd-analyze verify on the file before daemon-reload. Start it without enabling it first, inspect status and logs, then stop it. Use an absolute executable path and a dedicated user so the unit does not depend on an interactive shell. Finally, inspect the effective unit with systemctl cat hello.service and confirm it matches the file you intended to load.

What each line buys you

Description= is what you see in systemctl status and in the journal. Make it specific.

ExecStart= is the one required line. Absolute path to the binary, then its arguments.

User=hello drops privileges before the process runs. If someone finds a bug in the server, they get the hello account, not root.

Restart=on-failure is the restart boundary. Crashes and non-zero exits get a restart. A clean systemctl stop does not.

WantedBy=multi-user.target only matters when you enable the unit. It tells systemd where to put the symlink.

Validate before you load

Here is the verify step in practice:

systemd-analyze verify /etc/systemd/system/hello.service

Silence means the file parsed. A typo in a directive name prints something like this:

/etc/systemd/system/hello.service:6: Unknown key name 'Users' in section 'Service', ignoring.

Notice the word “ignoring”. Without the verify step, systemd would load the unit and run it as root. The typo is silent at runtime. This is why I never skip the check.

Try this with a real server: put a two-line Node.js HTTP server in /opt/hello/server.js, make it readable by the hello user, and walk through the sequence above. Start it, curl it, stop it. The next lesson covers what happens when you load it into the manager.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →