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.

Steps to create a systemd service unit:

  1. Create the script or command wrapper that the service will run.
    #!/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.

  2. Make the script 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.

  3. Create the service unit file.
    [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.

  4. Verify that the new unit parses cleanly before reloading the manager.
    $ 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.

  5. Reload the systemd manager so it notices the new unit file.
    $ 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.

  6. Enable the service for boot and start it immediately.
    $ 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.

  7. Confirm that the service is loaded from the local unit path and running.
    $ 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.

  8. Confirm that the workload is actually doing its job.
    $ 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.