Create a systemd Service Unit in Linux

Deepak Prasad
Tested on RHEL 10.2 (Coughlan) — vm1.lab.example
Package systemd 257-23.el10_2.2
Applies to RHEL, Rocky Linux, AlmaLinux, Fedora, Ubuntu, Debian, and other Linux hosts using systemd as PID 1
Privilege sudo or root to create files under /etc/systemd/system/ and manage units
Scope Write a Type=simple service unit, reload systemd, start, enable, stop, and verify. Covers [Unit], [Service], and [Install] fields used in a minimal custom unit. Does not cover timers, socket activation, or boot/shutdown hook ordering (see linked guides).
Related guides systemctl command
Beginners guide to systemd
Run script at boot with systemd
systemd timers
Run script before shutdown

A systemd service unit is a .service file that tells systemd how to start, stop, and supervise a process. This walkthrough builds a small demo-heartbeat.service on vm1.lab.example that prints a timestamp every 30 seconds to stdout (captured by the journal), then manages it with systemctl.


Where service unit files live

Custom units belong under /etc/systemd/system/. Package managers install vendor units elsewhere:

Directory Purpose
/etc/systemd/system/ Administrator-created units and systemctl enable symlinks — put custom units here
/usr/lib/systemd/system/ Units shipped with RPM/DEB packages (path may be /lib/systemd/system/ on Debian/Ubuntu)
/run/systemd/system/ Runtime-generated units; can override vendor units until reboot

Files in /etc/systemd/system/ take precedence over vendor units with the same name.


Write the program the service will run

Store custom administrator scripts in /usr/local/sbin/ rather than temporary directories such as /tmp:

bash
sudo tee /usr/local/sbin/demo-heartbeat.sh <<'EOF'
#!/bin/bash
while true; do
  echo "time=$(date -Is)"
  sleep 30
done
EOF

Make it executable:

bash
sudo chmod 755 /usr/local/sbin/demo-heartbeat.sh

Run the script for two seconds so you can confirm the output format without leaving a background loop running:

bash
sudo timeout 2 /usr/local/sbin/demo-heartbeat.sh

Sample output:

output
time=2026-08-21T16:54:18+05:30

That line goes to stdout. When systemd supervises the script, the same output lands in the journal for the unit.


Create the unit file

A service normally uses [Unit] for metadata and dependencies, [Service] for the process itself, and [Install] when the unit should support systemctl enable.

Create /etc/systemd/system/demo-heartbeat.service:

ini
[Unit]
Description=Demo heartbeat service

[Service]
Type=simple
ExecStart=/usr/local/sbin/demo-heartbeat.sh
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Description= is the name systemctl status displays. This demo needs no After= or Before= line because the script has no dependency on another unit — add those only when the service genuinely must start after something else, such as After=network-online.target for a boot script that calls a remote API.

Type=simple fits a script whose main process stays in the foreground. Restart=on-failure lets systemd retry after a non-zero exit without looping on a clean stop from systemctl stop. WantedBy=multi-user.target is what systemctl enable uses to start the unit on boot.

For a script that runs once and exits, use Type=oneshot instead — see run a script at boot with systemd.


Reload systemd and verify the unit file

Reload the unit cache so systemd reads the new file:

bash
sudo systemctl daemon-reload

daemon-reload exits silently on success.

Check the unit syntax before you start it:

bash
systemd-analyze verify /etc/systemd/system/demo-heartbeat.service

When verify passes, it prints nothing.


Start the service and check status

Start the unit on the running system:

bash
sudo systemctl start demo-heartbeat.service

Wait a couple of seconds, then read the active state:

bash
systemctl status demo-heartbeat.service --no-pager

Sample output:

output
● demo-heartbeat.service - Demo heartbeat service
     Loaded: loaded (/etc/systemd/system/demo-heartbeat.service; disabled; preset: disabled)
     Active: active (running) since Fri 2026-08-21 16:54:31 IST; 2s ago
   Main PID: 101112 (demo-heartbeat.)
      Tasks: 2 (limit: 48735)
     Memory: 592K (peak: 1.2M)
        CPU: 25ms
     CGroup: /system.slice/demo-heartbeat.service
             ├─101112 /bin/bash /usr/local/sbin/demo-heartbeat.sh
             └─101116 sleep 30

Aug 21 16:54:31 vm1.lab.example systemd[1]: Started demo-heartbeat.service - Demo heartbeat service.

Active: active (running) and a Main PID line confirm systemd is supervising the script. Read the heartbeat lines from the journal:

bash
journalctl -u demo-heartbeat.service -n 5 --no-pager

Sample output:

output
Aug 21 16:54:31 vm1.lab.example systemd[1]: Started demo-heartbeat.service - Demo heartbeat service.
Aug 21 16:54:31 vm1.lab.example demo-heartbeat.sh[101112]: time=2026-08-21T16:54:31+05:30

The demo-heartbeat.sh[PID]: line is stdout from your script — that is the output journalctl -u is meant to show.


Enable the service at boot

Enable the unit for future boots:

bash
sudo systemctl enable demo-heartbeat.service

Sample output:

output
Created symlink '/etc/systemd/system/multi-user.target.wants/demo-heartbeat.service' → '/etc/systemd/system/demo-heartbeat.service'.

Confirm systemd will start it on boot:

bash
systemctl is-enabled demo-heartbeat.service

Sample output:

output
enabled

The symlink under multi-user.target.wants/ is the persistent enable marker.


Stop, disable, and remove the unit

Stop the running service:

bash
sudo systemctl stop demo-heartbeat.service

Confirm it is no longer active:

bash
systemctl is-active demo-heartbeat.service

Sample output:

output
inactive

Disable it so it does not start on the next boot:

bash
sudo systemctl disable demo-heartbeat.service

Sample output:

output
Removed '/etc/systemd/system/multi-user.target.wants/demo-heartbeat.service'.

To remove the unit entirely, delete the unit file and script:

bash
sudo rm -f /etc/systemd/system/demo-heartbeat.service /usr/local/sbin/demo-heartbeat.sh

Reload systemd so it drops the removed unit from its cache:

bash
sudo systemctl daemon-reload

Both commands exit silently when the files are gone and systemd has re-read the unit set.


Choose a service Type

Type=simple works for this foreground script. Other applications may require exec, oneshot, notify, or forking depending on how they start — check the upstream unit file or man page before you copy a Type= value from another service.


Troubleshooting

Symptom Likely cause Fix
Unit demo-heartbeat.service not found Typo in name or file not under /etc/systemd/system/ Fix path and name; run systemctl daemon-reload
203/EXEC in status Bad ExecStart= path or missing execute permission Fix path, chmod 755, move script to /usr/local/sbin
Changes to the unit ignored systemd still using cached unit systemctl daemon-reload after every edit
Service exits immediately with status=0 on oneshot work Wrong Type= for a long-running script Use Type=simple when the process should keep running
Service enters failed state ExecStart process exited non-zero Check systemctl status and journalctl -u demo-heartbeat.service for the actual error
Failed to enable unit Invalid [Install] section Ensure WantedBy= points at an existing target such as multi-user.target

References


Summary

You create a systemd service by placing a .service file under /etc/systemd/system/ with [Unit], [Service], and usually [Install], pointing ExecStart= at a script or binary systemd should supervise. After systemctl daemon-reload, start brings the unit up, status shows the active state, and journalctl -u captures stdout from the service. enable adds the multi-user.target.wants/ symlink for boot.

The mistake I see most often is editing the unit file but skipping daemon-reload, or adding After= without a real dependency. Match Type= to how the process starts — simple for a foreground loop like this demo, oneshot for a one-time boot script — and keep custom units in /etc/systemd/system/.

For broader systemctl verbs, see the systemctl command reference. For boot, timer, and shutdown timing, use the linked systemd guides in the intro table.


Frequently Asked Questions

1. Where do I put a custom systemd service file?

Create the unit under /etc/systemd/system/name.service on most distributions. Package units live under /usr/lib/systemd/system/; do not edit those files directly because updates overwrite them.

2. What is the difference between Type=simple and Type=oneshot?

Type=simple is for a long-running process whose main command stays in the foreground. Type=oneshot runs a command once and exits; pair it with RemainAfterExit=yes when later starts should treat the unit as already done.

3. Why must I run systemctl daemon-reload after editing a unit?

systemd caches unit files at reload time. daemon-reload makes systemd read new or changed files under /etc/systemd/system/ before start, enable, or restart.

4. What does WantedBy=multi-user.target do?

WantedBy= is used by systemctl enable. It creates a symlink under multi-user.target.wants/ so the service starts during a normal multi-user boot.

5. How do I check whether my service is running?

Run systemctl status name.service for the active state and recent journal lines. Use journalctl -u name.service -n 20 to read stdout and stderr captured from the service process.
Omer Cakmak

Linux Administrator

Highly skilled at managing Debian, Ubuntu, CentOS, Oracle Linux, and Red Hat servers. Proficient in bash scripting, Ansible, and AWX central server management, he handles server operations on OpenStack, KVM, Proxmox, and VMware.

  • Debian
  • Ubuntu
  • Linux
  • Red Hat Enterprise Linux
  • Shell Script
  • System Administration