Why automate tasks on a VPS?
A VPS is a server running continuously, often without direct human supervision. That is precisely why automating recurring tasks is essential: nobody will be around at 3 AM to trigger a backup or renew a Let's Encrypt certificate. A forgotten task can mean data loss, an expired certificate taking the site offline, or a disk full because log rotation never ran. The two main tools available on Linux — cron and systemd timers — allow you to schedule these operations reliably.
Typical use cases on a VPS
- Backups — MySQL or PostgreSQL dumps, file archiving to remote storage
- TLS certificate renewal —
certbot reneworacme.sh --cronscheduled twice a day - Log rotation and cleanup — deleting files older than 30 days, weekly compression
- Health checks — verifying a service is listening, auto-restarting if needed
- Data synchronization —
rsyncto a second node, cache refresh - Environment cleanup — deleting expired sessions, emptying temporary directories
cron: a quick refresher
Cron is present on virtually every Linux system. To schedule a task, edit the current user's crontab with crontab -e. The syntax uses five space-separated fields: minute, hour, day of month, month, day of week, followed by the command. For example, 0 3 * * * /usr/local/bin/backup.sh runs the script every day at 3 AM. Files placed in /etc/cron.d/ belong to the system and can specify the execution user directly on the line. Cron keeps no native history of past runs and does not handle missed jobs if the machine was off.
systemd timers: an introduction
A systemd timer is a pair of two unit files: a .service file describing what to run, and a .timer file describing when to trigger it. The timer is activated with systemctl enable --now myservice.timer and managed by the service manager, exactly like any other systemd service. The OnCalendar= directive accepts rich syntax: daily, Mon *-*-* 03:00:00, *:0/15 (every 15 minutes). All output goes through journald and is accessible via journalctl -u myservice.service. The command systemctl list-timers shows all active timers, their next deadline and last activation.
cron vs systemd timers: comparison table
| Criterion | cron | systemd timer |
|---|---|---|
| Schedule syntax | 5-field `* * * * *` | `OnCalendar=` readable (`daily`, `Mon 03:00`) |
| Logs | Mail output or manual redirect | Native journald, `journalctl -u` |
| Missed jobs (machine off) | Lost (unless `anacron`) | `Persistent=true` replays after reboot |
| Inter-service dependencies | None | `After=`, `Requires=`, `Wants=` |
| Isolation and resource limits | Inherits shell env | Cgroups, `MemoryMax=`, `CPUQuota=` |
| Run as specific user | User field in `/etc/cron.d/` | `User=` and `Group=` in `.service` |
| Debugging | Hard without logs | `systemctl status`, `journalctl -xe` |
| Availability | All Unix systems | Systems with systemd (Debian, Ubuntu, RHEL…) |
When to keep cron?
Cron remains the right choice in several situations. On older or minimal systems without systemd — some containers, BSD, Alpine images — cron is often the only tool available. In a multi-user environment where each user manages their own tasks via crontab -e, cron is simpler to delegate without granting root rights. For very simple one-liner scripts, the crontab stays readable and maintainable without creating two unit files. The deciding criterion is the need for logs, dependencies or resource constraints: if you do not need them, cron does the job perfectly.
When to switch to systemd timers?
Systemd timers become the clear choice as soon as the task goes beyond a simple isolated command. You need journald to trace every execution, its output and return code without manual plumbing? Systemd timers. Your backup script must only run after PostgreSQL is ready (After=postgresql.service)? Systemd timers. You want to cap the memory of a sync job so it does not starve other processes (MemoryMax=512M)? Systemd timers. And if the server restarts at 2:58 AM when the 3 AM backup was due, Persistent=true guarantees it will run at next boot.
Create a systemd timer from scratch (example: daily backup)
Create the service file
Open /etc/systemd/system/backup.service and enter: [Unit], Description=Daily PostgreSQL backup, After=postgresql.service, then [Service], Type=oneshot, User=postgres, ExecStart=/usr/local/bin/backup.sh. The oneshot type indicates the service exits after the script completes.
Create the timer file
Create /etc/systemd/system/backup.timer with: [Unit], Description=Daily trigger for backup, then [Timer], OnCalendar=*-*-* 03:00:00, Persistent=true, and [Install], WantedBy=timers.target.
Reload systemd and enable the timer
Run systemctl daemon-reload so systemd picks up the new files, then systemctl enable --now backup.timer to activate and immediately start the timer.
Check the timer
Type systemctl list-timers --all to see your timer in the list with its next and last execution dates. Use systemctl status backup.timer for timer state, and systemctl status backup.service for the result of the last execution.
Read the logs
Access all service output with journalctl -u backup.service for the full history, or journalctl -u backup.service -n 50 --since today for the last 50 lines today.
Migrate an existing cron job to systemd
Identify the cron job to migrate
List your crontabs with crontab -l (current user) and cat /etc/cron.d/* (system). Note the exact command, the execution user, and the five-field schedule. For example: 30 2 * * 1 root /usr/bin/certbot renew --quiet means every Monday at 2:30 AM as root.
Translate the schedule to OnCalendar
The OnCalendar= format reads DayOfWeek Year-Month-Day Hour:Minute:Second. 30 2 * * 1 becomes Mon *-*-* 02:30:00. To test the translation before applying it, use systemd-analyze calendar 'Mon *-*-* 02:30:00' which displays the next 10 calculated occurrences.
Create the .service and .timer files
Create /etc/systemd/system/certbot-renew.service with Type=oneshot, ExecStart=/usr/bin/certbot renew --quiet, and User=root. Then create /etc/systemd/system/certbot-renew.timer with OnCalendar=Mon *-*-* 02:30:00 and Persistent=true. Run systemctl daemon-reload && systemctl enable --now certbot-renew.timer.
Disable the cron line and validate
Comment out or delete the line in the original crontab. Test immediately with systemctl start certbot-renew.service and check the result via journalctl -u certbot-renew.service -n 20. Verify the timer appears in systemctl list-timers.
For a task that must run a few minutes after boot and then regularly, combine OnBootSec=5min and OnUnitActiveSec=1h in the [Timer] block. The job starts 5 minutes after boot, then every hour from there — without depending on a fixed clock time.
Common systemd timer troubleshooting
Three problems come up frequently when setting up timers. First: the timer is active but never triggers. Check with systemctl list-timers that the NEXT column shows a coherent date, and that systemctl status backup.timer shows active (waiting). A forgotten daemon-reload after modifying a unit file is the most common cause. Second: the service fails silently. Check journalctl -u myservice.service -n 50. Verify that the ExecStart= path is absolute. Third: missed tasks are not replayed after reboot. Make sure Persistent=true is present in the [Timer] block.
Conclusion
Cron and systemd timers coexist without issue on the same server — you do not have to migrate everything at once. Keep cron for your simple tasks and existing user scripts, and adopt systemd timers for new automations that benefit from journald logs, inter-service dependencies or cgroup isolation.