Skip to content

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 .service file goes and how it should be named;
  • distinguish between the main Type= values (simple, forking, oneshot);
  • apply the most important [Service] directives, such as ExecStart=, Restart=, User=;
  • explain why daemon-reload is 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:

/opt/hello/hello.sh
#!/usr/bin/env bash
while true; do
    echo "Hello, $(date)"
    sleep 10
done
sudo chmod +x /opt/hello/hello.sh

Now create a unit file for this script:

sudo nano /etc/systemd/system/hello.service
/etc/systemd/system/hello.service
[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:

sudo systemctl daemon-reload

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:

sudo systemctl enable --now hello.service

Check it:

systemctl status hello.service
● 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:

sudo systemctl reset-failed hello.service
sudo systemctl start hello.service

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

  1. Create the hello.service example above in a test VM, bring it up with daemon-reload and enable --now, and watch its output with journalctl -u hello.service -f (covered in depth in the next article, journalctl).
  2. Confirm the Restart=on-failure policy works by killing the process with kill -9.
  3. Change Restart=on-failure to Restart=no, run daemon-reload and restart, then kill the process again with kill -9 and explain the difference.
  4. Deliberately change Type=simple to Type=oneshot and 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.

References