Creating a custom service unit lets systemd start a local worker, wrapper script, or in-house daemon the same way it starts packaged services. That matters when a command should survive reboots, restart after failures, and expose a normal service state instead of living in ad-hoc shell sessions or boot scripts.
A service unit combines a [Unit] description, a [Service] execution policy, and usually an [Install] section that tells systemctl enable where the unit should attach during boot. Current upstream systemd.unit documentation places administrator-created system units under /etc/systemd/system ahead of /usr/local/lib/systemd/system and /usr/lib/systemd/system, while current systemd.service documentation notes that Type=simple is the default when ExecStart= is set but recommends Type=exec for long-running custom commands because startup failures are reported more accurately.
The example below creates a long-running system service named custom-worker.service that launches /usr/local/bin/custom-worker.sh and writes a heartbeat line to a log file. Root access is required for /etc/systemd/system and /usr/local/bin, ExecStart= should point to an absolute executable path, and programs that daemonize themselves or only run one short action need a different service type than the one shown here. After writing the unit, run systemd-analyze verify, then systemctl daemon-reload, before enabling or starting it.
#!/usr/bin/env bash while :; do printf "custom-worker tick %s\n" "$(date --iso-8601=seconds)" >> /var/log/custom-worker.log sleep 30 done
Use a helper script when the workload needs shell features such as loops, redirection, or several commands. Use an absolute path so the unit does not depend on an interactive shell environment to find the executable.
$ sudo chmod 0755 /usr/local/bin/custom-worker.sh
If the script was prepared somewhere else first, sudo install -m 0755 source-file /usr/local/bin/custom-worker.sh copies it into place and sets the mode in one step.
[Unit] Description=Custom worker service demo [Service] Type=exec ExecStart=/usr/local/bin/custom-worker.sh Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
Type=exec is a good default for long-running custom commands because systemd reports a start failure if the executable cannot be invoked. Restart=on-failure restarts the service after an unexpected non-zero exit, and current upstream systemd.special documentation recommends WantedBy=multi-user.target for services that should come up in a normal non-graphical boot.
Most services managed by systemd should stay in the foreground. If the application daemonizes itself by default, disable that mode or adjust the service type instead of copying this example unchanged. Add User= and Group= when the service should run as a dedicated account instead of root.
$ sudo systemd-analyze verify /etc/systemd/system/custom-worker.service
No output is the ideal result. Current upstream systemd-analyze verify behavior also loads referenced units, so a warning can point at some other existing unit file; read the filename on each message before assuming the new service unit is wrong.
$ sudo systemctl daemon-reload
Current upstream systemctl documentation states that daemon-reload reruns generators, reloads unit files, and recreates the dependency tree, so the new definition becomes visible to the manager without a reboot.
$ sudo systemctl enable --now custom-worker.service Created symlink /etc/systemd/system/multi-user.target.wants/custom-worker.service → /etc/systemd/system/custom-worker.service.
enable --now combines the persistent boot-time install step with the immediate start action. Use enable without --now when the service should wait until the next boot or until some other dependency starts it.
$ systemctl status --no-pager --full custom-worker.service | head -n 7
● custom-worker.service - Custom worker service demo
Loaded: loaded (/etc/systemd/system/custom-worker.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-04-13 21:15:06 +08; 12ms ago
Main PID: 1785 (bash)
Tasks: 2 (limit: 4543)
Memory: 532.0K (peak: 952.0K)
CPU: 3ms
The success state for this example is Loaded: loaded (/etc/systemd/system/custom-worker.service…) together with Active: active (running). The exact PID, task count, memory, and timestamp will differ on another host.
$ tail -n 3 /var/log/custom-worker.log custom-worker tick 2026-04-13T21:15:06+08:00
An application-specific output file, socket, API response, or queue action is often a stronger proof than systemctl status alone because it shows the service logic is really running, not just that the process stayed alive.
If the file stays empty or the unit falls back to failed, inspect the recent journal with sudo journalctl -u custom-worker.service -n 20 –no-pager.