Creating a Custom systemd Service
Up to this point, we've only managed ready-made .service files installed by packages. But a significant part of an administrator's job is turning software you wrote yourself, or installed manually, into a service just like those — one managed through systemctl, started automatically at boot, and restarted automatically after a crash. This article writes a new .service file from scratch; Deploying a Backend App as a Service then applies the same knowledge to a full, realistic scenario with a real backend application.
What You Will Learn
By the end of this article you will be able to:
- know where a new
.servicefile goes and how it should be named; - distinguish between the main
Type=values (simple,forking,oneshot); - apply the most important
[Service]directives, such asExecStart=,Restart=,User=; - explain why
daemon-reloadis required after creating or editing a unit file; - carry out the full cycle of starting and verifying a new service.
A Minimal Working Example
Suppose /opt/hello/hello.sh is a script that prints a message every 10 seconds in an infinite loop:
Now create a unit file for this script:
[Unit]
Description=Hello demo service
After=network.target
[Service]
Type=simple
ExecStart=/opt/hello/hello.sh
Restart=on-failure
[Install]
WantedBy=multi-user.target
Info
New unit files always go into /etc/systemd/system/ — the highest-priority directory covered in systemd Units and Targets. /lib/systemd/system/ is reserved for files managed by the package manager.
Type=: Describing How a Service Starts
systemd needs to know when to consider a service "successfully started" — that's exactly what Type= communicates:
Type value |
Startup shape | When it's used |
|---|---|---|
simple (default) |
The ExecStart= process runs in the foreground, never forking |
Modern applications and daemons that don't fork on their own (most Node.js, Python, Go programs) |
forking |
The ExecStart= process forks itself, the parent exits, the child continues |
Classic Unix daemons (e.g. nginx in its default mode) |
oneshot |
The command runs once and exits, with no ongoing process | A script or one-time setup action (e.g. preparation that must run before another service) |
notify |
The program tells systemd it's ready via sd_notify() |
Programs that implement the sd_notify protocol |
In practice, backend applications (Python, Node.js, Go, Java) almost always use Type=simple, since these run in the foreground without forking.
Warning
Choosing the wrong Type= confuses the reported service state. For example, applying Type=simple to a program that actually forks means systemd considers the service "stopped" as soon as the parent process exits, even though the child process is still running.
The Key [Service] Directives
| Directive | Purpose |
|---|---|
ExecStart= |
The command run when the service starts (must be an absolute path) |
ExecStop= |
A custom command to stop the service (if omitted, systemd sends a standard signal) |
ExecReload= |
The command run when systemctl reload is invoked |
Restart= |
When to automatically restart the service: no, on-failure, always, and others |
RestartSec= |
How long to wait before restarting (in seconds) |
User= / Group= |
Which user/group the service runs as |
WorkingDirectory= |
The service's working directory |
EnvironmentFile= |
An external file the service reads environment variables from |
[Service]
Type=simple
User=hello
WorkingDirectory=/opt/hello
ExecStart=/opt/hello/hello.sh
Restart=on-failure
RestartSec=5
Restart=on-failure restarts the service automatically if it exits with a non-zero (error) code or is killed by a signal, but not if it was stopped normally with systemctl stop. Restart=always restarts on any exit whatsoever (including cases beyond a plain stop) — this is reserved for more specific cases.
Running a service under User=/Group= instead of root is an important security practice, covered in more depth in Deploying a Backend App as a Service.
Activating a New Unit File
Once a unit file is created or edited, systemd does not "see" it immediately — it only rereads unit files when it starts, or when explicitly told to:
daemon-reload tells systemd to reread all unit files from disk. This command must be run every time a file under /etc/systemd/system/ is created or changed — otherwise systemd keeps working with the old, in-memory configuration.
Now start the service and enable it at boot:
Check it:
● hello.service - Hello demo service
Loaded: loaded (/etc/systemd/system/hello.service; enabled; preset: enabled)
Active: active (running) since Fri 2026-07-24 11:05:02 UTC; 12s ago
Main PID: 5211 (hello.sh)
Tasks: 2 (limit: 4665)
Memory: 1.1M
CPU: 15ms
CGroup: /system.slice/hello.service
├─5211 /bin/bash /opt/hello/hello.sh
└─5215 sleep 10
Active: active (running) combined with enabled means the service is both running right now and will start automatically on the next boot — matching the state table covered in Service Management.
Testing the Restart= Policy
sudo kill -9 $(systemctl show -p MainPID --value hello.service)
sleep 6
systemctl status hello.service
kill -9 forcibly kills the process (simulating an unexpected crash). With RestartSec=5 configured, a few seconds later systemctl status should show the service running again with a new PID.
When Auto-Restart Gives Up: Start-Rate Limiting
Restart= does not retry forever. systemd counts restarts within a time window, and if a service restarts too many times too quickly, it stops trying and drops the unit into failed:
hello.service: Start request repeated too quickly.
hello.service: Failed with result 'start-limit-hit'.
Two directives in the [Unit] section control that window — StartLimitIntervalSec= (default 10s) and StartLimitBurst= (default 5): more than Burst starts inside Interval trips the limit. This is deliberate — it keeps a genuinely broken service (a bad config, a missing dependency) from burning CPU in an endless crash loop. Once the underlying cause is fixed, the counter has to be cleared before the service will start again:
Interview angle
"A service with Restart=always is stuck in failed and won't come back — why?" is a common scenario question. The answer is almost always the start limit: the process is crashing on startup faster than StartLimitBurst/StartLimitIntervalSec allows, so systemd gave up on purpose. The right move is to read journalctl -u <unit> for the crash cause, fix it, then reset-failed — not to raise the limit, which only hides the loop.
Common Mistakes
Forgetting daemon-reload
If a unit file is edited without running daemon-reload, systemctl restart runs with the old configuration — the change appears not to be "taking effect," even though the file itself was saved correctly.
Using a Relative Path in ExecStart=
systemd services do not inherit a working directory unless WorkingDirectory= is explicitly set. A relative path like ExecStart=./hello.sh often fails with "No such file or directory" — always use an absolute path (/opt/hello/hello.sh).
Running a Service as root When It Doesn't Need To
Without a User= directive, a service runs as root by default. If the program doesn't actually need root privileges, this creates unnecessary security risk — as shown in Deploying a Backend App as a Service, creating a dedicated user is the recommended approach.
Exercises
- Create the
hello.serviceexample above in a test VM, bring it up withdaemon-reloadandenable --now, and watch its output withjournalctl -u hello.service -f(covered in depth in the next article, journalctl). - Confirm the
Restart=on-failurepolicy works by killing the process withkill -9. - Change
Restart=on-failuretoRestart=no, rundaemon-reloadandrestart, then kill the process again withkill -9and explain the difference. - Deliberately change
Type=simpletoType=oneshotand observe what changes (pay attention to the service's resulting state).
Verification criterion: you should be able to explain, for a service that appears "not to update" after an edit, the first command to run to fix it.
Summary
A new .service file is created inside /etc/systemd/system/ and must contain at least Description=, ExecStart=, and — for automatic startup at boot — a WantedBy= value. Type= describes how the service starts, while Restart=/RestartSec= define its recovery policy after a failure. Any change to a unit file requires daemon-reload, or systemd keeps working from the old configuration. The next article, Deploying a Backend App as a Service, applies this exact knowledge to a real backend project, adding security and environment-configuration practices.