CKH Notes

Run Scripts with systemd Services and Timers on Ubuntu 24.04

Test environment and hardware

ItemTested configuration
Test date2026-10-02
Operating systemUbuntu 24.04.5 LTS, amd64
Test hosts1 fresh isolated VM(s); resources below are per VM
CPU2 vCPU
Memory4 GiB
System disk32 GiB, HDD-backed, VirtIO, ext4
Linux kernel6.8.0-139-generic
systemd255.4-1ubuntu8.17

Run a script with systemd under a dedicated account and limited writable paths, then schedule it with a timer. Verify success, an intentional exit-code-7 failure, repair, and reboot persistence.

This tutorial was tested on fresh, isolated Ubuntu 24.04 virtual machines with synthetic data. Packages came from signature-verified sources for Noble. No production database or website credentials were used. Check the sources and versions appropriate for your own environment.

The screenshots show actual bash PTY output from the test machines, rendered in a browser terminal view. The prompt is normalized to lab$; results were not rewritten. This English edition preserves the tested commands and original images. Some code comments, sample strings, and screenshot footers remain in Traditional Chinese. Copyable commands are provided separately.

1. Create a dedicated account and directories

sudo useradd --system --home /var/lib/demo --shell /usr/sbin/nologin demo
sudo install -d -o demo -g demo -m 0750 /opt/demo /var/lib/demo
sudo tee /opt/demo/tick.sh >/dev/null <<'EOF'
#!/bin/sh
set -eu
date -u +%FT%TZ >> /var/lib/demo/runs.log
echo "demo tick completed"
EOF
sudo chown root:root /opt/demo/tick.sh
sudo chmod 0755 /opt/demo/tick.sh

The account has no interactive shell. Root manages the script; output goes to a dedicated data directory. Each run writes one timestamp. It exposes no network service or secret.

2. Use a oneshot service with necessary writable paths

# /etc/systemd/system/demo.service
[Unit]
Description=Tutorial oneshot task

[Service]
Type=oneshot
User=demo
Group=demo
ExecStart=/opt/demo/tick.sh
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/demo

Type=oneshot suits a task that exits after finishing. Without RemainAfterExit, inactive after success is normal. Check Result=success and ExecMainStatus=0, not only active state.

3. Create the daily timer

# /etc/systemd/system/demo.timer
[Unit]
Description=Tutorial timer

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
Unit=demo.service

[Install]
WantedBy=timers.target
sudo systemd-analyze verify /etc/systemd/system/demo.service /etc/systemd/system/demo.timer
sudo systemctl daemon-reload
sudo systemctl enable --now demo.timer
sudo systemctl start demo.service
systemctl show demo.service -p Result -p ExecMainStatus
systemctl is-enabled demo.timer
sudo journalctl -u demo.service -n 5 --no-pager -o cat
Successful oneshot task and enabled timer.
Figure 1. Successful oneshot task and enabled timer. Actual PTY capture; click for full size.

Calendar time follows the host timezone: Asia/Taipei in this VM, while the script records UTC. Persistent=true can catch up a qualifying missed calendar activation when the timer starts again. It does not implement retries or guarantee each job succeeds.

4. Observe an actual short timer trigger

sudo systemd-run --unit=demo-trigger-verified --on-active=3s \
  --timer-property=AccuracySec=1ms /usr/bin/systemctl start demo.service
sudo tail /var/lib/demo/runs.log

A new written timestamp proved the timer actually called the service. AccuracySec=1ms is used for the short test to avoid default wake-up coalescing obscuring the result. The daily timer retains ordinary settings. Success was recorded only after observing the new output.

5. Reproduce failure and inspect the exit code

sudo tee /opt/demo/fail.sh >/dev/null <<'EOF'
#!/bin/sh
exit 7
EOF
sudo chmod 0755 /opt/demo/fail.sh
sudo tee /etc/systemd/system/demo-fail.service >/dev/null <<'EOF'
[Service]
Type=oneshot
User=demo
ExecStart=/opt/demo/fail.sh
EOF
sudo systemctl daemon-reload
sudo systemctl start demo-fail.service
systemctl show demo-fail.service -p Result -p ExecMainStatus
Intentional service failure with exit code 7.
Figure 2. Intentional service failure with exit code 7. Actual PTY capture; click for full size.

The failed service start is intentional. ExecMainStatus=7 matches the script’s exit code. Repair the script, reset the failed state, and rerun:

sudo tee /opt/demo/fail.sh >/dev/null <<'EOF'
#!/bin/sh
echo "repaired task completed"
EOF
sudo chmod 0755 /opt/demo/fail.sh
sudo systemctl reset-failed demo-fail.service
sudo systemctl start demo-fail.service
systemctl show demo-fail.service -p Result -p ExecMainStatus

6. Check behavior after reboot

systemctl is-enabled demo.timer
systemctl is-active demo.timer
systemctl list-timers demo.timer --no-pager
sudo systemctl start demo.service
systemctl show demo.service -p Result -p ExecMainStatus

After reboot, the timer was active and the task still ran. Add production failure notifications, retry policies, and data backups as needed. Logs containing credentials or business data must remain private.

Verified results

VerificationResult
Dedicated-account oneshot succeededPassed
Timer configuration and enablement passedPassed
Three-second timer produced a new output recordPassed
Exit-code-7 failure reproducedPassed
Repair returned exit code 0Passed
Timer healthy after rebootPassed

Scope: a real accelerated three-second trigger, calendar configuration, and boot enablement. The test did not wait until the next day’s 03:30 schedule or measure long-term punctuality, external notifications, or resistance to attacks on every sandbox option.

Official references

github.com/systemd/systemd/blob/v255/man/systemd.service.xml · github.com/systemd/systemd/blob/v255/man/systemd.timer.xml