| 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:
sudo tee /usr/local/sbin/demo-heartbeat.sh <<'EOF'
#!/bin/bash
while true; do
echo "time=$(date -Is)"
sleep 30
done
EOFMake it executable:
sudo chmod 755 /usr/local/sbin/demo-heartbeat.shRun the script for two seconds so you can confirm the output format without leaving a background loop running:
sudo timeout 2 /usr/local/sbin/demo-heartbeat.shSample output:
time=2026-08-21T16:54:18+05:30That 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:
[Unit]
Description=Demo heartbeat service
[Service]
Type=simple
ExecStart=/usr/local/sbin/demo-heartbeat.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetDescription= 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:
sudo systemctl daemon-reloaddaemon-reload exits silently on success.
Check the unit syntax before you start it:
systemd-analyze verify /etc/systemd/system/demo-heartbeat.serviceWhen verify passes, it prints nothing.
Start the service and check status
Start the unit on the running system:
sudo systemctl start demo-heartbeat.serviceWait a couple of seconds, then read the active state:
systemctl status demo-heartbeat.service --no-pagerSample 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:
journalctl -u demo-heartbeat.service -n 5 --no-pagerSample 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:30The 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:
sudo systemctl enable demo-heartbeat.serviceSample 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:
systemctl is-enabled demo-heartbeat.serviceSample output:
enabledThe symlink under multi-user.target.wants/ is the persistent enable marker.
Stop, disable, and remove the unit
Stop the running service:
sudo systemctl stop demo-heartbeat.serviceConfirm it is no longer active:
systemctl is-active demo-heartbeat.serviceSample output:
inactiveDisable it so it does not start on the next boot:
sudo systemctl disable demo-heartbeat.serviceSample output:
Removed '/etc/systemd/system/multi-user.target.wants/demo-heartbeat.service'.To remove the unit entirely, delete the unit file and script:
sudo rm -f /etc/systemd/system/demo-heartbeat.service /usr/local/sbin/demo-heartbeat.shReload systemd so it drops the removed unit from its cache:
sudo systemctl daemon-reloadBoth 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.

