Introduction
Cron has been the go‑to scheduler on Unix for decades. It’s simple and everywhere—but on modern Linux, it also spreads jobs across multiple crontabs, hides failures unless you wire up mail/logging, and can’t easily express operational needs like backfilling missed runs, sandboxing, or per‑job resource limits. Systemd timers keep the simplicity of “run X at time Y,” while giving you first‑class observability (journalctl), resilient catch‑up (Persistent=true), and strong unit‑level controls (users, cgroups, security hardening). If you manage production servers—or just want scheduled tasks on a laptop that sleeps—you’ll likely prefer timers.
This article is a pragmatic guide to migrating from cron to systemd timers. We’ll build a mental model (service does the work; timer decides when), create and enable units, map common cron patterns to OnCalendar, add hardening and resource limits, verify with real logs, and troubleshoot the sharp edges. Copy‑pasteable templates are included throughout.
Goal: Give admins a clear, copy‑pasteable path to migrate cronjobs to systemd timers, with explanations, templates, and troubleshooting.
Why replace cron with systemd timers?
- Unified management:
systemctlcontrols creation, enablement, status, logs, and sandboxing. - Better logging: Output goes to
journaldout of the box (journalctl -u your.service). - Reliability:
Persistent=truemakes timers catch up on missed runs (e.g., rebooted host or laptop asleep). - Flexibility: Calendar syntax (like cron) plus monotonic triggers (run X minutes after boot/last run).
- Security & resource controls: Use
User=/DynamicUser=,ProtectSystem=,PrivateTmp=,Nice=,CPUQuota=,MemoryMax=, etc.
When not to replace cron:
- You only need a single, simple system-wide job and you already have working cron+logging.
- You need non-systemd environments (containers/base images without systemd).
How systemd timers work (the 60‑second tour)
- A .service unit defines the work to do.
- A .timer unit defines when to start the service.
- Timers can be calendar (e.g., 02:15 daily) or monotonic (e.g., 15 minutes after boot, or every 5 minutes since last activation).
- Timers live beside services under
/etc/systemd/system/(system-wide) or~/.config/systemd/user/(per-user, no root required). - Timers are enabled/started like services, and their next/last run is visible via
systemctl list-timers.
Rule of thumb: one
.timertriggers one.serviceof the same base name (e.g.,backup.timer→backup.service).
Quick start: create, install, run, and verify
We’ll migrate a cron job that runs /usr/local/bin/backup.sh every day at 02:15.
1) Create the service unit
Create /etc/systemd/system/backup.service:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
[Unit] Description=Nightly backup After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh # (optional) Hardening & resource controls User=backup Group=backup WorkingDirectory=/ Nice=10 IOSchedulingClass=best-effort IOSchedulingPriority=6 ProtectSystem=full ProtectHome=true PrivateTmp=true NoNewPrivileges=true CapabilityBoundingSet= RestrictSUIDSGID=true LockPersonality=true # (optional) Environment # Environment="AWS_PROFILE=prod" "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin" [Install] # Usually not enabled directly; the timer is enabled instead. |
Tip: Keep the job’s logic in a script and call it from
ExecStart=. That keeps the unit tiny and makes local testing easy.
2) Create the timer unit
Create /etc/systemd/system/backup.timer:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[Unit] Description=Nightly backup timer [Timer] OnCalendar=*-*-* 02:15:00 Persistent=true AccuracySec=1min RandomizedDelaySec=0 Unit=backup.service [Install] WantedBy=timers.target |
Key fields:
OnCalendar: calendar expression (see templates below).Persistent=true: catch up after downtime.AccuracySec: jitter window; set lower for punctual jobs.RandomizedDelaySec: add jitter for fleets to avoid thundering herds.
3) Reload & enable
|
1 2 3 |
sudo systemctl daemon-reload sudo systemctl enable --now backup.timer |
4) Verify schedule and logs
|
1 2 3 4 5 6 7 8 9 |
# Show timers (system-wide) systemctl list-timers --all | grep backup # What does the calendar expression mean? systemd-analyze calendar "*-*-* 02:15:00" # Show service logs journalctl -u backup.service --since "yesterday" |
5) Run ad‑hoc for testing
|
1 2 3 4 5 6 7 |
# Trigger the service immediately sudo systemctl start backup.service # Or schedule a one-off run 2 minutes from now (no unit files needed) sudo systemd-run --on-active=2m --unit=backup-oneoff /usr/local/bin/backup.sh journalctl -u backup-oneoff |
User timers vs system timers
- System timers:
/etc/systemd/system/*.timer, managed withsudo, run as specifiedUser=(or root if omitted). - User timers:
~/.config/systemd/user/*.timer, managed withsystemctl --user, run as your user without sudo.- Enable lingering to allow timers when not logged in:
loginctl enable-linger <username>.
- Enable lingering to allow timers when not logged in:
Examples (user scope):
|
1 2 3 4 5 |
systemctl --user daemon-reload systemctl --user enable --now report.timer systemctl --user list-timers journalctl --user -u report.service |
Calendar syntax cheat‑sheet (cron → systemd)
Systemd’s OnCalendar supports rich expressions. Some handy forms:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# Daily at 02:15 OnCalendar=*-*-* 02:15:00 # Every 5 minutes OnCalendar=*:0/5 # Hourly at minute 17 OnCalendar=*-*-* *:17:00 # Weekdays at 09:00 (Mon..Fri) OnCalendar=Mon..Fri 09:00 # Weekends at 10:30 (Sat,Sun) OnCalendar=Sat,Sun 10:30 # First Sunday of the month at 03:00 OnCalendar=Sun *-*-01..07 03:00:00 # Last day of the month at 23:55 OnCalendar=*-*-* 23:55:00 AccuracySec=1h # combine with RandomizedDelaySec if desired # Specific dates (e.g., Jan 1 at 00:00 every year) OnCalendar=*-01-01 00:00:00 # Quarterly on the 1st at 02:00 OnCalendar=2025-01..12/3-01 02:00:00 # At boot +15m, then every 6h thereafter (monotonic) OnBootSec=15min OnUnitActiveSec=6h |
DST & time zones: Systemd timers interpret times in the system’s localtime unless you use
UTCsuffix (e.g.,02:00 UTC). On DST transitions, systemd attempts to run at the correct local time; for skipped times, it schedules the next valid occurrence.
Cron → systemd mapping table
| Cron | Meaning | OnCalendar equivalent |
|---|---|---|
*/5 * * * * |
Every 5 minutes | *:0/5 |
17 * * * * |
At minute 17 past every hour | *-*-* *:17:00 |
15 2 * * * |
02:15 daily | *-*-* 02:15:00 |
0 9 * * 1-5 |
09:00 Mon–Fri | Mon..Fri 09:00 |
30 10 * * 6,7 |
10:30 Sat & Sun | Sat,Sun 10:30 |
0 3 1 * * |
03:00 on the 1st of each month | *-*-01 03:00:00 |
0 0 1 1 * |
Midnight every Jan 1 | *-01-01 00:00:00 |
0 3 * * 0 |
Sundays at 03:00 | Sun 03:00 |
0 23 L * * |
Last day of month 23:00 (Vixie cron extension) | *-*-* 23:00 + constrain with date math: *-*-28..31 23:00 & AccuracySec=1h (or run daily and guard in script) |
For complex “last weekday” styles, prefer a small wrapper script that exits non‑zero unless today matches the rule.
Example: migrate a cronjob step‑by‑step
Original crontab line (root):
|
1 2 |
15 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1 |
Service: /etc/systemd/system/backup.service
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[Unit] Description=Nightly backup [Service] Type=oneshot ExecStart=/usr/local/bin/backup.sh # Log is handled by journald; optional file logging shown below StandardOutput=journal StandardError=inherit # Optional legacy file log mirror # ExecStart=/bin/sh -c '/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1' [Install] |
Timer: /etc/systemd/system/backup.timer
|
1 2 3 4 5 6 7 8 9 10 11 |
[Unit] Description=Nightly backup timer [Timer] OnCalendar=*-*-* 02:15:00 Persistent=true AccuracySec=1min [Install] WantedBy=timers.target |
Commands
|
1 2 3 4 5 |
sudo systemctl daemon-reload sudo systemctl enable --now backup.timer systemctl list-timers | grep backup journalctl -u backup.service --since today |
Monotonic timers (not cron‑like, but super useful)
Monotonic timers trigger relative to events (boot, when the timer was enabled, when its service last started or stopped), instead of at specific wall‑clock times. They’re perfect for “every N minutes/hours” jobs, staggered warmups after boot, and periodic health checks that shouldn’t care about time zones or DST.
Key fields
OnActiveSec=— run N after this timer is activated (e.g., afterenable --noworstart).OnBootSec=— run N after system boot.OnStartupSec=— run N after the system manager starts (slightly later than boot; rarely needed on servers).OnUnitActiveSec=— run N after the triggered unit (the.service) was last activated (i.e., started).OnUnitInactiveSec=— run N after the triggered unit was last deactivated (i.e., finished/exited).AccuracySec=— scheduling window; larger values coalesce wakeups and save power.RandomizedDelaySec=— add jitter to distribute load across fleets.WakeSystem=— ifyesand the platform supports RTC wake, the system can wake from sleep for the trigger.
Monotonic timers measure intervals using uptime clocks and don’t care about wall‑clock jumps or DST. They also don’t backfill past runs that would have occurred while the machine was powered off; the interval starts counting from boot or the last activation event.
Common patterns (copy‑paste)
Warm up 10 minutes after boot, then hourly
|
1 2 3 4 5 |
[Timer] OnBootSec=10min OnUnitActiveSec=1h RandomizedDelaySec=2min |
Use when a job needs the system to settle before the first run.
Health check every 5 minutes, forever
|
1 2 3 4 |
[Timer] OnActiveSec=5min AccuracySec=30s |
Starts 5 minutes after enabling the timer; then every 5 minutes. Good for watchdogs.
Run 2 minutes after the previous run finished
|
1 2 3 |
[Timer] OnUnitInactiveSec=2min |
Useful when you want a cool‑down between completions of the job.
Short, frequent poll with jitter (avoid dog‑piling)
|
1 2 3 4 5 |
[Timer] OnActiveSec=1min RandomizedDelaySec=15s AccuracySec=15s |
Add jitter on fleets to spread work.
One‑shot at boot + 30s (no repeats)
|
1 2 3 4 5 |
[Timer] OnBootSec=30s AccuracySec=5s # no On*Sec repeat fields → fires once per boot |
Great for cache warming or seeding state.
Wake a sleeping laptop for a maintenance task every night at uptime intervals
|
1 2 3 4 5 |
[Timer] OnBootSec=5min OnUnitActiveSec=1d WakeSystem=yes |
Note: This is uptime‑based; if you need wall‑clock 02:00 local, use a calendar timer instead.
Choosing between OnActiveSec, OnUnitActiveSec, and OnUnitInactiveSec
| Field | Interval measured from | When to use |
|---|---|---|
OnActiveSec= |
When the timer became active | Simple “every N” starting after enable; independent of the service runtime. |
OnUnitActiveSec= |
When the service started | “Every N since last start” even if the service runs a long time. |
OnUnitInactiveSec= |
When the service finished | “Wait N after completion” before the next run; avoids overlap naturally. |
If your service runtime varies and you must avoid overlap, prefer
OnUnitInactiveSec=and cap runtime withRuntimeMaxSec=or guard withflockin the script.
Service companion settings (to keep intervals sane)
|
1 2 3 4 5 6 7 8 9 |
[Service] Type=oneshot ExecStart=/usr/local/bin/job.sh # Prevent runaway jobs from collapsing your schedule RuntimeMaxSec=30m TimeoutStartSec=30m # Optional: ensure a single instance ExecStartPre=/usr/bin/flock -n /run/job.lock -c true |
Verifying monotonic schedules
|
1 2 3 4 5 6 7 8 9 10 |
# Show next/last triggers systemctl list-timers --all | sed -n '1,5p; /job/p' # Watch runs and durations in real time journalctl -fu job.service # Inspect effective unit definitions systemctl cat job.timer systemctl cat job.service |
Practical examples
Example A — Log rotate helper every 6 hours regardless of DST
/etc/systemd/system/log-rotate-helper.service
|
1 2 3 4 |
[Service] Type=oneshot ExecStart=/usr/local/sbin/log-rotate-helper.sh |
/etc/systemd/system/log-rotate-helper.timer
|
1 2 3 4 5 6 7 8 |
[Timer] OnActiveSec=6h AccuracySec=5min RandomizedDelaySec=5min Unit=log-rotate-helper.service [Install] WantedBy=timers.target |
Example B — Run backups after boot delay, then once per day after each completion
|
1 2 3 4 5 6 |
[Timer] OnBootSec=15min OnUnitInactiveSec=1d RandomizedDelaySec=10min Unit=backup.service |
Ensures one backup per day with a 15‑minute post‑boot grace and no overlap even if backups take hours.
Example C — Retry worker with cool‑down
|
1 2 3 |
[Timer] OnUnitInactiveSec=10min |
Pair with exit codes in the service. If you need exponential backoff, build it into the script or re‑queue via systemd-run.
Gotchas & tips
- Monotonic timers don’t catch up for time spent powered off. If catch‑up semantics are required (e.g., “run once for each missed day”), use a calendar timer with
Persistent=trueand let the script skip if not needed. - Intervals are counted from the chosen reference event; large
AccuracySeccan delay triggers slightly—set it consciously for SLO‑critical tasks. RandomizedDelaySecapplies to both calendar and monotonic timers; it’s your friend on multi‑host fleets.- For user timers on laptops, consider
WakeSystem=yesbut balance against battery life.
Observability & verification
- List timers:
systemctl list-timers --all - Explain next runs:
systemd-analyze calendar 'Mon..Fri 02:30' - Show unit config:
systemctl cat job.service job.timer - Logs:
journalctl -u job.service(system) orjournalctl --user -u job.service(user) - Dry‑run calendar:
systemd-analyze calendar --iterations=5 'Sat *-*-01..07 03:00'
Email notifications on failure
There are two clean ways to alert on failures:
- Systemd‑native handler using
OnFailure=→ triggers a separate.servicewhen your job fails (non‑zero exit, timeout, OOM, etc.). - In‑script notification → your script emails when it detects an error. This works everywhere but mixes concerns.
Recommended: OnFailure= with a template notifier
Add to your job’s service unit (not the timer):
|
1 2 3 4 5 |
[Unit] Description=Nightly backup OnFailure=notify@%n.service OnFailureJobMode=replace |
Create a reusable notifier template /etc/systemd/system/notify@.service:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[Unit] Description=Send failure email for %I [Service] Type=oneshot # Where to send Environment=MAIL_TO=ops@example.com # Optionally load from a file to avoid hardcoding # EnvironmentFile=/etc/default/systemd-notify # Compose subject and body; include last logs ExecStart=/bin/bash -lc 'subject="FAIL: %I on %H"; \ journalctl -u %I -n 200 --no-pager --since "-2h" | \ mail -s "$subject" "$MAIL_TO"' # Alternatives if you prefer sendmail/postfix/msmtp: # ExecStart=/usr/sbin/sendmail -t <<EOF # To: $MAIL_TO # Subject: FAIL: %I on %H # # $(journalctl -u %I -n 200 --no-pager --since "-2h") # EOF |
This template receives the failing unit name in
%I(instance of%n), gathers recent logs, and emails them. Swapsendmail,msmtp, or an HTTP webhook viacurlas needed.
Reload and test:
|
1 2 3 4 5 6 |
sudo systemctl daemon-reload # Force a failure (make your script exit 1) then: sudo systemctl start backup.service || true # Check notifier fired systemctl status 'notify@backup.service' |
Tip: If your script is chatty, tighten the log slice by changing -n 200 or the --since window.
Per‑user timers (no root) with notifications
For --user units, create ~/.config/systemd/user/notify@.service with the same content and use a mailer available to the user (e.g., msmtp). Enable linger if you want notifications when logged out: loginctl enable-linger <user>.
Alternative: notify inside the script
Keep it simple with Bash’s || operator and set -euo pipefail:
|
1 2 3 4 5 6 7 |
#!/usr/bin/env bash set -euo pipefail trap 'code=$?; if [[ $code -ne 0 ]]; then \ tail -n 200 /var/log/myjob.log | mail -s "FAIL: myjob on $(hostname) (exit $code)" ops@example.com; fi' EXIT # …do work… |
Pros: portable. Cons: mixes job logic with alerting and depends on local mail config.
Sending only on repeated failures
Use systemd’s restart policy plus a notifier hooked to a separate “give up” unit:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# myjob.service [Service] Type=oneshot ExecStart=/usr/local/bin/myjob.sh Restart=on-failure RestartSec=1min StartLimitBurst=3 StartLimitIntervalSec=30min [Unit] StartLimitAction=none OnFailure=notify@%n.service |
Or, run the notifier from a path unit that watches a flag file your script writes only after N consecutive failures.
Minimizing noise
- Include a Runbook URL in the email body.
- Add the timer schedule and host:
%H(hostname) andsystemctl cat <unit>snippets help responders. - Consider routing through a central alerting system (e.g., use
curlto post to Opsgenie/PagerDuty/Slack webhook) instead of raw email.
Troubleshooting
Timer never fires
systemctl status job.timer— is it loaded, enabled, and active?systemctl list-timers --all | grep job— confirm next run time.- Missing
WantedBy=timers.targetin[Install]? Re‑enable:systemctl enable --now job.timer. - Wrong path or permissions in
ExecStart=? Checkjournalctl -u job.service. - For user timers when logged out, enable lingering:
loginctl enable-linger <user>.
Missed during reboot/maintenance window
- Add
Persistent=trueto the[Timer]section.
Wrong time zone / DST surprises
- Confirm
timedatectlshows the expected local timezone. - Use explicit
UTCinOnCalendarwhen coordinating cross‑region jobs.
Command needs PATH or env vars
- Set
Environment=in the service or use anEnvironmentFile=/etc/default/jobname. - Always specify absolute paths in
ExecStart=.
Service keeps running forever
- Add
TimeoutStartSec=10min(or an appropriate limit) to the[Service].
Multiple jobs pile up
- Add
RefuseManualStart=/RefuseManualStop=if needed. - Use
ConcurrencyPolicy=isn’t available in systemd; instead setExecStartPre=/bin/systemd-run --scope --property=...or use a lock file in the script. Simpler: guard your script withflock.
Needs root?
- Prefer least privilege: run as a non‑root
User=/Group=with adequate permissions. Usesudowithin the script only if absolutely necessary.
SELinux/AppArmor denials
- Check
journalctl -t setroubleshoot(RHEL family) ordmesgfor AVCs; adjust policies or relax hardening options.
Hardening & resource controls (copy‑paste snippets)
Minimal sandbox for safe scripts:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[Service] User=jobuser Group=jobuser NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true PrivateDevices=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true LockPersonality=true CapabilityBoundingSet= RestrictNamespaces=true RestrictSUIDSGID=true UMask=0077 WorkingDirectory=/ |
Resource limits:
|
1 2 3 4 5 6 7 8 |
[Service] Nice=10 CPUQuota=25% IOWeight=200 MemoryMax=1G RuntimeMaxSec=1h TimeoutStartSec=30min |
Patterns & templates library
Hourly, at minute 0
|
1 2 3 4 5 |
[Timer] OnCalendar=hourly AccuracySec=1min Persistent=true |
Every N minutes
|
1 2 3 4 |
[Timer] OnCalendar=*:0/5 AccuracySec=1min |
Business days at 08:00 America/New_York
|
1 2 3 4 |
[Timer] OnCalendar=Mon..Fri 08:00 Timezone=America/New_York |
Weekly maintenance, Sunday 02:30 UTC with jitter
|
1 2 3 4 5 |
[Timer] OnCalendar=Sun 02:30 UTC RandomizedDelaySec=30min AccuracySec=5min |
Quarter-hourly with catch-up after downtime
|
1 2 3 4 |
[Timer] OnCalendar=*:0/15 Persistent=true |
At boot + 10m, then every day
|
1 2 3 4 5 |
[Timer] OnBootSec=10min OnUnitActiveSec=1d RandomizedDelaySec=5min |
Safe migration checklist
Use this end‑to‑end flow to move a fleet from cron to timers with minimal risk.
- Inventory everything
|
1 2 3 4 5 |
# System crontab & cron.d sudo awk '{print FILENAME ": " $0}' /etc/crontab /etc/cron.d/* 2>/dev/null || true # Per‑user crontabs for u in $(getent passwd | cut -d: -f1); do sudo crontab -u "$u" -l 2>/dev/null | sed "s|^|$u: |"; done |
- Extract logic into scripts
- Put each job’s logic in
/usr/local/bin/<job>.shwith absolute paths, strict mode, and clear exit codes.
|
1 2 3 4 |
#!/usr/bin/env bash set -euo pipefail # …your job… |
- Create a service unit per job
- Minimal
Type=oneshot, pointExecStart=at the script, and setUser=to the least‑privileged account. - Add sandboxing (see Hardening & resource controls below).
- Create a matching timer
- Translate the cron expression to
OnCalendar=(use the mapping table orsystemd-analyze calendar 'expr'). - Add
Persistent=trueif you need catch‑up after downtime.
- Load and enable
|
1 2 3 |
sudo systemctl daemon-reload sudo systemctl enable --now <job>.timer |
- Verify schedule & next run
|
1 2 3 |
systemctl list-timers --all | grep <job> || true systemd-analyze calendar "<OnCalendar>" |
- Smoke‑test the service
|
1 2 3 |
sudo systemctl start <job>.service journalctl -u <job>.service -n 100 --no-pager |
- Run the first scheduled execution under watch
- Keep
journalctl -f -u <job>.serviceopen during the first real run. - Tune
AccuracySec/RandomizedDelaySecto fit your environment (tight for single hosts, jitter for fleets).
- Disable the original cron entries
- Comment out lines in crontabs or remove files under
/etc/cron.d/only after the timer proves reliable.
- Document & standardize
- Commit unit files to config management.
- Adopt naming:
<app>-<purpose>.service+<app>-<purpose>.timer. - Add a runbook with: how to start/stop, where logs live, SLO (max duration), owner/escalation.
Quick audit loop (post‑migration)
|
1 2 3 4 5 |
# Timers that failed recently journalctl -p err -u '*.service' --since '24 hours ago' # Services exceeding runtime SLO (example: >30m) journalctl -u '*.service' --since '24 hours ago' | awk '/Starting|Finished/ {print $0}' |
FAQ
Where do I put units?
- System-wide:
/etc/systemd/system/ - Per-user:
~/.config/systemd/user/
How do I uninstall?
|
1 2 3 4 |
sudo systemctl disable --now job.timer sudo rm /etc/systemd/system/job.timer /etc/systemd/system/job.service sudo systemctl daemon-reload |
Can a timer trigger multiple services?
- Use one timer per service. For fan‑out, trigger a wrapper script that calls others, or use a target/wants relationship.
What about systemd.path units?
- Use
.pathto trigger on filesystem events (e.g., when a file appears). Combine with.timerif you need both time- and event-driven runs.
Appendix: Useful commands reference
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Timers overview systemctl list-timers --all # Inspect units systemctl cat name.service systemctl cat name.timer # Status & logs systemctl status name.timer journalctl -u name.service # Calendar helper\ssystemd-analyze calendar "Mon..Fri 09:00" # One-off scheduled run systemd-run --on-calendar="2025-12-01 03:00" /path/to/job.sh |
Ready-to-use skeleton
/etc/systemd/system/job.service
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
[Unit] Description=My scheduled job After=network-online.target Wants=network-online.target [Service] Type=oneshot User=jobuser Group=jobuser WorkingDirectory=/ ExecStart=/usr/local/bin/job.sh StandardOutput=journal StandardError=inherit NoNewPrivileges=true ProtectSystem=full ProtectHome=true PrivateTmp=true [Install] |
/etc/systemd/system/job.timer
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
[Unit] Description=Timer for my scheduled job [Timer] OnCalendar=Mon..Fri 09:00 Persistent=true AccuracySec=1min RandomizedDelaySec=0 Unit=job.service [Install] WantedBy=timers.target |
That’s it! With these templates and checks, you can confidently migrate cron to systemd timers, gain better observability, and harden scheduled jobs with modern Linux tooling.




