Define one finite piece of work
Imagine a personal tool that rebuilds a small daily summary from its existing records. The job reads a bounded set of data, prepares one replacement summary and exits. It is not a web server and should not keep running between scheduled starts. Write down the input, destination, expected duration and what counts as success before adding a timer.
The example assumes a Linux host with systemd and util-linux flock, administrator access, and an existing unprivileged account and group named fieldjob. Its reviewed job script lives at /opt/field-jobs/build-summary and runs in the foreground. An administrator owns that script; the job account can read and execute it but cannot rewrite its code. Check executable paths on your distribution. These are templates, not an installed OffVPS scheduling service.
The script must report failure with a nonzero exit status. For replacement files, design it to prepare and check a temporary result before publishing it. For database work, use the database's transaction or deduplication mechanism. A lock alone cannot make a partially completed task safe to repeat.
Choose a timezone and a missed-run policy
Our summary is useful once a day at 02:15 UTC. Use an explicit zone so that the machine's local timezone is not an invisible dependency. UTC avoids seasonal clock changes in this example; a business task tied to local civil time needs a named timezone and a review of its daylight-saving behavior. The systemd time reference describes calendar expressions.
Save this template as /etc/systemd/system/field-summary.timer on the intended VPS after reviewing the names:
[Unit]
Description=Daily personal-tool summary
[Timer]
OnCalendar=*-*-* 02:15:00 UTC
Persistent=true
Unit=field-summary.service
[Install]
WantedBy=timers.target
Persistent=true requests a catch-up activation after an inactive period if a calendar event was missed. It does not replay a separate run for every missed day. A timer also does not start another instance of the same service while that service is active. These behaviors, and scheduling accuracy, are covered by the timer reference. Decide whether a late summary is useful before enabling catch-up.
Set the service user, state directory and lock
The matching /etc/systemd/system/field-summary.service describes the work:
[Unit]
Description=Build the personal-tool summary
[Service]
Type=oneshot
User=fieldjob
Group=fieldjob
WorkingDirectory=/opt/field-jobs
StateDirectory=field-summary
StateDirectoryMode=0700
UMask=0077
ExecStart=/usr/bin/flock --nonblock --conflict-exit-code 75 /var/lib/field-summary/job.lock /opt/field-jobs/build-summary
TimeoutStartSec=5min
StandardOutput=journal
StandardError=journal
Type=oneshot suits a command that completes; an explicit startup timeout bounds this example. Do not add RemainAfterExit=yes to a service that must become inactive after each run. See systemd's service semantics. Set a timeout from the workload, and make interruption safe rather than assuming five minutes fits every job.
For this system service, StateDirectory creates the named directory under /var/lib with service ownership. The working directory, user, permissions and output destinations are explicit; the script should use absolute paths for its own tools and data. Environment and directory rules are documented in systemd's execution reference. Do not put secret values in command-line arguments or log them.
The nonblocking flock wrapper exits with code 75 when another cooperating invocation holds this lock. We deliberately leave that as a visible failure needing review, rather than treating a skipped summary as completed work. All manual invocations must use the same wrapper and lock path. Do not delete a lock file to release a running job: a new file can create a separate lock. This is a local-filesystem pattern, not a distributed lock across VPS instances. See the upstream flock manual.
Check the files, then try one controlled run
Use a disposable dataset with outbound email, payments and other side effects disabled for the first run. Before activation, inspect the parsed schedule and unit files on that test host:
systemd-analyze calendar '*-*-* 02:15:00 UTC'
systemd-analyze verify /etc/systemd/system/field-summary.service /etc/systemd/system/field-summary.timer
These checks can catch unit and schedule mistakes; they cannot establish that your script produces a correct summary. The systemd-analyze reference explains their scope. After correcting errors, load the reviewed files and explicitly start the test job:
sudo systemctl daemon-reload &&
sudo systemctl start field-summary.service
systemctl status field-summary.service --no-pager
sudo journalctl -u field-summary.service --since "10 minutes ago" --no-pager
Expect a finite successful invocation and a checked output artifact; a successful oneshot may show inactive afterward. Inspect the result itself, including empty-input behavior. Make a test-only run deliberately slow, invoke the same locked command twice on the test dataset, and verify that the second invocation reports lock contention without writing a competing result.
Enable the schedule only after checking its output
sudo systemctl enable --now field-summary.timer
systemctl list-timers field-summary.timer --all
Check the next scheduled time, then review the first scheduled result. Keep start time, finish time, processed item count and outcome in the script's logs. An absent success record is a reason to inspect, not proof that the job ran. Refer to systemctl's timer and activation commands.
To pause future starts, use sudo systemctl disable --now field-summary.timer. That does not terminate an already running service. If interruption is needed, first establish whether its current write can safely stop. As work grows, review duration, retries and external API limits; multiple machines require shared coordination. Continue with the personal tools scenario and a restore exercise for the data this automation maintains.
Documentation used
Primary references for this page. Check the documentation for the version installed in your own environment.