Creating a systemd template unit lets one reusable unit definition start many named service instances. That fits workers, listeners, or helper jobs that all run the same command but need different instance names, paths, or labels.

A template unit places one @ before the unit type suffix, such as demo-worker@.service. When systemd is asked to start demo-worker@tenant-api.service, it first looks for that literal file and then falls back to the matching template file if only demo-worker@.service exists. Inside the unit, %i expands to the escaped instance name and %I expands to the unescaped form.

System template units belong under /etc/systemd/system and require root access. Create the template, verify it, reload the manager, and then start or enable a named instance. systemd-analyze verify can also report warnings from other unit files on the same host, so check the filename on each warning before assuming the new template is wrong.

Steps to create a systemd template unit:

  1. Convert the first instance label into a valid instantiated unit name.
    $ systemd-escape --template=demo-worker@.service tenant/api
    demo-worker@tenant-api.service

    Use the escaped unit name in systemctl commands. If an instance label is already a simple unit-safe string such as backup-db, you can use it directly without the helper.

  2. Create the helper script that each instance will run.
    #!/usr/bin/env bash
    install -d -m 0755 /var/log/demo-worker-template
    printf 'instance=%s\nunescaped=%s\n' "$1" "$2" > "/var/log/demo-worker-template/$1.log"

    This demo script writes one small log file per instance so the template behavior stays visible. Replace it with the real workload after the unit structure is working.

  3. Make the helper script executable.
    $ sudo chmod 0755 /usr/local/bin/demo-worker-template.sh

    Use an absolute executable path in the unit file so the service does not depend on an interactive shell PATH.

  4. Create the template unit file.
    [Unit]
    Description=Demo worker for %I
     
    [Service]
    Type=oneshot
    ExecStart=/usr/local/bin/demo-worker-template.sh %i %I
    RemainAfterExit=yes
     
    [Install]
    WantedBy=multi-user.target

    %i passes the escaped instance name such as tenant-api, and %I passes the unescaped value such as tenant/api. RemainAfterExit=yes keeps this oneshot demo visible as active (exited) after it finishes. Add DefaultInstance=alpha only when systemctl enable demo-worker@.service should choose a default instance automatically.

  5. Verify that the new template parses cleanly before the manager loads it.
    $ sudo systemd-analyze verify /etc/systemd/system/demo-worker@.service

    No output from the new unit is the ideal result. If a warning appears, confirm which file it references before changing the new template.

  6. Reload the systemd manager so it notices the new template file.
    $ sudo systemctl daemon-reload

    This reloads unit files and rebuilds the dependency tree before the first instance is started or enabled.

  7. Enable and start the first named instance from the template.
    $ sudo systemctl enable --now demo-worker@tenant-api.service
    Created symlink /etc/systemd/system/multi-user.target.wants/demo-worker@tenant-api.service → /etc/systemd/system/demo-worker@.service.

    The symlink is named after the instance, but it still points back to the single template file. That is how one template can define many persistent instances.

  8. Confirm that the instance is active and backed by the template file.
    $ systemctl show demo-worker@tenant-api.service -p Id -p ActiveState -p SubState -p FragmentPath -p UnitFileState
    Id=demo-worker@tenant-api.service
    ActiveState=active
    SubState=exited
    FragmentPath=/etc/systemd/system/demo-worker@.service
    UnitFileState=enabled

    Look for FragmentPath=/etc/systemd/system/demo-worker@.service together with ActiveState=active and SubState=exited. A long-running template service would usually show SubState=running instead.

  9. Confirm that the workload received both instance values.
    $ cat /var/log/demo-worker-template/tenant-api.log
    instance=tenant-api
    unescaped=tenant/api

    The log shows why template specifiers matter: the instance name stays safely escaped for filenames and unit names, while the original label is still available to the workload through %I.

  10. Start a second instance from the same template.
    $ sudo systemctl start demo-worker@backup-db.service

    You do not need to create a second unit file. Another instance name is enough.

  11. List the instantiated services that now share the same template.
    $ systemctl list-units 'demo-worker@*.service' --all --no-pager
      UNIT                           LOAD   ACTIVE SUB    DESCRIPTION
      demo-worker@backup-db.service  loaded active exited Demo worker for backup/db
      demo-worker@tenant-api.service loaded active exited Demo worker for tenant/api
    
    ##### snipped #####
    
    2 loaded units listed.
    To show all installed unit files use 'systemctl list-unit-files'.

    Both instance names stay independent even though they both come from the single demo-worker@.service template file.