Build a Disposable systemd Oneshot Service
Build, run and validate a bounded systemd oneshot service using status and journal evidence, then roll it back safely with a clear path to production.

In this lesson
Table of Contents
Table of contents
Before you begin
- Use an isolated or non-production validation environment.
- Confirm product version and permissions before applying any change.
- Basic command-line familiarity, including creating text files and reading command output.
Track this tutorial
Choose your current status and tick each safety check as you complete it. Sign in to sync progress between devices.
Current status
Before you apply the change
Confirm these production-safety controls during the tutorial.
systemdThe KBY Lexiconsystemdsystemd is the Linux init system and service manager that starts, supervises and coordinates units of work such as services, sockets and timers. is the init system and service manager used by most current LinuxThe KBY LexiconLinuxLinux is the open-source kernel underlying most server, container and cloud infrastructure; distinct from the distributions built around it. distributions, and it underpins a large share of routine automation and service operations work: starting and stopping services, sequencing dependencies at boot, and running scheduled or triggered tasks reliably. For Automation and Service Operations practitioners, systemd is not simply a tool for keeping a daemon alive; it is a dependency graph, a privilege boundary, and a source of evidence that a task actually happened the way it was supposed to. Understanding how systemd represents work as discrete units, how those units relate to one another, and how it records the outcome of each run is the first-principles foundation for validating any automation built on top of it.
This guide builds and validates one bounded automation task: a systemd oneshot service that performs a small, disposable piece of work and produces evidence you can inspect. You will learn the terminology and trust boundaries involved, work through a fully explained example, run a safe exercise with explicit pass and stop conditions, and connect what you observe in a lab environment to the permissions and escalation practices required in production. Every command and every piece of evidence described here is tied to a reason, so you understand not just what to type but what outcome should exist afterwards and why.
#Learning Objectives
- Explain systemd’s unit model, dependency graph and privilege boundaries in terms relevant to automation and service operations.
- Build and load a bounded systemd oneshot service unit and connect each directive to its operational purpose.
- Validate a service run using systemctl and journalctl evidence rather than assumption.
- Diagnose common systemd automation failures using symptom, cause and correction reasoning.
- Apply safe rollback and cleanup steps that return a host to its original state.
- Identify what changes between a validated lab exercise and a production deployment, including permissions and escalation.
#Prerequisites
- Access to an isolated or non-production Linux host that uses systemd as its init system, with sudo or root access you are authorised to use.
- Confirmation of the host’s systemd version and distribution before applying any unit configuration, since some directive defaults differ between releases and should not be assumed.
- Basic command-line familiarity, including creating text files and reading command output.
- Willingness to remove or disable everything created during the exercise before treating the host as clean again.
#Content
#Mental Model and Terminology
A unit is systemd’s basic object of management: a service, a timer, a mount, a target, and several other kinds of managed object are all represented as units. A service unit (.service) describes a process systemd should start, stop and supervise. The Type= directive tells systemd how to interpret that process’s lifecycle. A oneshot service is a service that is expected to run once and exit, rather than remain resident like a long-running daemon; systemd waits for it to finish and treats a zero exit code as success. ExecStart= defines the command that actually runs. Dependency directives such as After= and Wants= describe ordering and soft dependency relationships between units, while Requires= expresses a hard dependency. A target is a synchronisation point, similar in spirit to a traditional runlevel, that groups units for a particular system state.
The systemd manager itself runs as process ID 1 with root privilege and is the single authority that reads unit files, resolves dependencies, forks the processes those units describe, and records what happened. When you run systemctl daemon-reload, you are asking that manager to re-read unit files from disk; nothing you write to a unit file takes effect until that happens.

#Components, Dependencies and Trust Boundaries
The most important trust boundary in this workflow is straightforward but easy to overlook: anything with permission to write a unit file into a directory systemd trusts, such as /etc/systemd/system/, or anything with permission to run systemctl against the system manager, can define a process that runs with whatever privilege that unit specifies. By default a service unit inherits root privilege from the manager unless you explicitly reduce it with User= and Group=. This is why validating a systemd automation task is inseparable from validating its privilege boundary: a unit that works correctly but runs unnecessarily as root is not yet a safe unit.
The data flow for a single run is consistent regardless of how the unit was triggered: the manager reads the unit definition, checks ordering and dependency directives, forks the process named in ExecStart=, waits for it to exit, records the exit status, and writes standard output and standard error to the systemd journal. Everything you validate in this guide is downstream of that flow.
Rendering diagram...
#Examples
The worked example below defines a bounded oneshot service called evidence-check.service. Its only job is to write a timestamped line to a scratch file, which gives us something concrete to validate afterwards.
1[Unit]
2Description=Bounded evidence-writing task for validation exercise
3After=network.target
4
5[Service]
6Type=oneshot
7User=labuser
8WorkingDirectory=/home/labuser/systemd-lab
9ExecStart=/bin/sh -c 'echo "evidence-check ran at $(date -Iseconds)" >> /home/labuser/systemd-lab/evidence.log'
10
11[Install]
12WantedBy=multi-user.targetInput: the unit file above, saved as /etc/systemd/system/evidence-check.service. Output after running sudo systemctl daemon-reload followed by sudo systemctl start evidence-check.service and then systemctl status evidence-check.service --no-pager is a status block reporting Active: inactive (dead) together with a process line ending in status=0/SUCCESS. Interpretation: for a oneshot service, “inactive” after starting is the expected end state, not a failure; the unit is not supposed to keep running. The evidence that matters is the recorded exit code and the corresponding line appended to evidence.log, confirmed with journalctl -u evidence-check.service --no-pager -n 20.
| Command | Purpose | Expected evidence |
|---|---|---|
| systemctl daemon-reload | Load the new unit definition | No output; exit status 0 |
| systemctl start evidence-check.service | Execute the oneshot task | Command returns promptly with no error |
| systemctl status evidence-check.service | Confirm outcome | status=0/SUCCESS, Active: inactive (dead) |
| journalctl -u evidence-check.service | Inspect logged behaviour | Timestamped log line matching the script |
#Exercises
Objective: confirm, using status and log evidence rather than assumption, that a bounded oneshot service executes correctly under a non-root user and leaves a verifiable trace, then remove it cleanly.
Setup: on an isolated host, check whether the account exists with id labuser. If it does not, create it with sudo useradd -m labuser. Then create the directory and give it to that user with sudo mkdir -p /home/labuser/systemd-lab and sudo chown labuser:labuser /home/labuser/systemd-lab. Create the unit file shown in the worked example at /etc/systemd/system/evidence-check.service (for example with sudoedit), and confirm labuser can write to the directory before starting the service.
Steps: run sudo systemctl daemon-reload, then sudo systemctl start evidence-check.service, then check evidence with systemctl status evidence-check.service --no-pager and journalctl -u evidence-check.service --no-pager -n 20. Finally, open evidence.log and confirm the new line was appended.
#Safety Notes
Caution: only run this exercise on an isolated or non-production host, and only point ExecStart= at a disposable script that writes to a scratch path you control. Do not adapt this unit to touch production data, other services, or shared files while learning the pattern.
Expected evidence: a status block showing status=0/SUCCESS, a matching journal entry, and a new line in evidence.log with a current timestamp.
Pass condition: all three pieces of evidence agree and the timestamp matches the time you ran the exercise.
Stop condition: if systemctl status reports a failed state, or the log line is missing, stop and move to the Common Mistakes and failure-mode guidance before retrying, rather than repeatedly re-running an unexplained failure.
Cleanup condition: the exercise is only complete once the unit is stopped, its file removed, daemon-reload re-run, and the scratch directory deleted. Run sudo systemctl stop evidence-check.service, sudo rm /etc/systemd/system/evidence-check.service, then sudo systemctl daemon-reload. The unit was never enabled, so no disable step is needed; if you did enable it, run sudo systemctl disable evidence-check.service before removing the file. Before deleting the scratch directory, verify the path with ls -la /home/labuser/systemd-lab and confirm it holds only lab files, then run rm -r /home/labuser/systemd-lab. If you created labuser only for this exercise, remove it separately once you are sure it is not used elsewhere.

#Validation Guidance
Validating a systemd automation task means distinguishing between “the command returned” and “the intended work happened as designed”. A oneshot service that exits 0 tells you the process it ran did not signal an error; it does not by itself tell you the process did the right thing. That is why this guide treats three independent pieces of evidence as the minimum bar: the recorded exit status from systemctl status, the journal entry from journalctl, and the actual artefact the task was supposed to produce. Agreement across all three is an inference that the task behaved as intended; any single piece of evidence on its own is only an observation. Running the exercise a second time and confirming the same success pattern repeats is a further check for idempotency, which matters because production automation is rarely run exactly once.
#Common Mistakes
- Treating “inactive” as failure for a oneshot unit. This is the expected end state once the process exits; check the recorded exit status instead of the activation state.
- Omitting WorkingDirectory= and assuming a convenient default path. Without it, the default working directory for system services is the root directory (
/), so relative paths in the command resolve there and can produce missing or misplaced artefacts even on a “successful” run. The worked example avoids this by using absolute paths. - Running the unit as root without a stated reason. Unless the task materially requires root, set User= and Group= explicitly; unexplained privilege is a defect, not a convenience.
- Specifying a User= account that does not exist. The service fails with status 217/USER; create the account or correct the name, then reload and restart.
- Scratch directory not writable by the service user. The shell redirect fails with a permission-denied error in the journal and the service reports a failure; fix ownership of the directory.
- Forgetting daemon-reload after editing the unit file. systemd keeps using its previously loaded definition until reload is run, which can make a fixed unit appear to still fail.
#Key Takeaways
- systemd represents automation as units connected by an explicit dependency graph, not as ad-hoc scripts.
- A oneshot service’s success is defined by its recorded exit status, not by whether it is still running.
- Validation requires independent evidence: status, journal, and the artefact the task was meant to produce.
- Privilege should be reduced deliberately with User=/Group=, never left at the default without a stated reason.
- Every exercise in this pattern needs a stop condition and a cleanup path, not just a success path.
#Production Bridge
In production, the same unit pattern carries additional obligations. Confirm who has write access to /etc/systemd/system/ and who can run privileged systemctl commands, since that access is equivalent to defining what runs with that privilege. Prefer a dedicated service account over root, and consider systemd’s sandboxing directives, such as ProtectSystem= and NoNewPrivileges=, once directive availability and defaults have been confirmed against the documentation for the installed systemd version. Do not enable a new unit for automatic start until it has been validated in a staging environment that mirrors production permissions. Escalate to the system or service owner before deploying any unit that touches shared infrastructure, and before changing dependency ordering on units you did not author. Close out every validation session with the same operational checks used in the lab: confirm exit status, confirm journal evidence, confirm the artefact, and confirm that rollback (stop, disable, remove, reload) returns the host to its prior state before deciding the task is genuinely safe to repeat.
#Related reading
About this page
Credited to
Dates
Published
Sources
Corrections
- : Corrected during a review of published articles. Details
Read-only scripts
- Windows devices with no BitLocker recovery key in Entra IDMicrosoft Entra ID, Windows
- Intune devices that haven't synced in N daysMicrosoft Intune
- Mailboxes forwarding to external addressesExchange Online
- BitLocker status of every volume on this PCWindows
Upcoming deadlines
- Intune moves to macOS 15 and laterQ4 2026
- SAP SuccessFactors provisioning: move from basic auth to workload identityNovember 2026
- Intune November release: JIT registration reminder at APNs renewal and automatic JIT for eligible tenantsNovember 2026
Explore KBY
The KBY Brief
A short email about twice a week with new runbooks, configuration traps and fixes from KBY Technologies.
SubscribeRelated articles
Automation and Service Operations
Publishing a Production Knowledge-Base Article Safely
A production-safe walkthrough for graduate engineers on drafting, reviewing, staging, approving, publishing, and rolling back knowledge-base articles.
DevOps & Automation
GitHub Actions Docker Build: Fixing Permission Denied on docker.sock
Diagnose docker.sock permission denied in GitHub Actions by runner type, then apply a least-privilege fix with validation and rollback.
Enterprise IT Management
Bounded Enterprise IT Management Operations in Microsoft 365
A method for one bounded Microsoft 365 change: scope, baseline, pilot, observation and rollback, with evidence defined before action.
Discover more
Lexicon Definitions
Learn More About KBY
About KBY
Learn about our mission, how our content is produced, and how we handle corrections.
Why Trust KBY
How KBY content is produced, what is checked automatically, and how errors are corrected.
The KBY Brief
A short email about twice a week with new runbooks, configuration traps and fixes from KBY Technologies.
Was this useful?
Reports go to the site owner and are used to correct pages. Corrections are listed at /corrections.
Build practical engineering skills.
Receive new lessons, learning paths, practical exercises and early-career guidance.
Comments
Add a thoughtful note on Build a Disposable systemd Oneshot Service. Comments are checked for spam and held for moderation before appearing.