Automating server maintenance with cron jobs means writing small shell scripts that do one job each, then letting cron run them on a schedule you control. This guide builds a cleanup task, a backup task and log rotation for those tasks, all with locking and logging so you can see what happened and stop two runs from colliding.
You need SSH access to a Linux VPS and about 30 minutes. The examples work on Ubuntu 22.04/24.04, Debian 12, AlmaLinux 8–9 and Rocky 8–9.
Before you start
- A Linux VPS with a non-root sudo user, as described in create a sudo user on Linux. The examples use
deploy. - SSH keys already configured so you can log in without a password — see set up SSH keys if you have not done that yet.
- Confirm cron is installed and running:
systemctl status cronon Ubuntu/Debian,systemctl status crondon AlmaLinux/Rocky. - Decide which directories your own applications write to. Only touch paths you recognise; the scripts below use placeholders like
/home/deploy/app.
Warning: Cleanup jobs delete files. Always run a find command once with -print only to see what would be removed, before you add -delete. Never point a delete rule at /, /etc, /usr or a database data directory.
Create the maintenance workspace
Keep scripts in /usr/local/sbin (root-owned, not writable by normal users) and logs in /var/log/maintenance.
sudo mkdir -p /usr/local/sbin /var/log/maintenance
sudo chown root:root /usr/local/sbin /var/log/maintenance
sudo chmod 755 /usr/local/sbin /var/log/maintenanceEvery script starts with the same safety header. set -Eeuo pipefail makes the script stop on the first failed command, undefined variable or broken pipe instead of continuing with bad state.
#!/usr/bin/env bash
set -Eeuo pipefailWrite a safe cleanup script
Create /usr/local/sbin/maintenance-cleanup.sh. It removes old temporary files and an application cache directory, and appends a timestamped line to a log so you can check the result later.
sudo tee /usr/local/sbin/maintenance-cleanup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
LOG="/var/log/maintenance/cleanup.log"
STAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
{
echo "[$STAMP] cleanup started on $(hostname)"
find /var/tmp -xdev -type f -mtime +14 -print -delete
find /home/deploy/app/storage/cache -xdev -type f -mtime +7 -print -delete 2>/dev/null || true
echo "[$STAMP] cleanup finished"
} >> "$LOG" 2>&1
EOF
sudo chmod 750 /usr/local/sbin/maintenance-cleanup.sh-xdev stops find from crossing into another mounted filesystem, so a mounted volume or NFS share is never touched by accident. Adjust -mtime +14 to the retention you actually want.
Test it by hand before cron ever sees it:
sudo /usr/local/sbin/maintenance-cleanup.sh
sudo tail -n 20 /var/log/maintenance/cleanup.logAdd an off-server backup task
Cron is only useful for backups if the copy leaves the machine. A minimal example that archives a directory and keeps 7 days locally:
sudo tee /usr/local/sbin/maintenance-backup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
DEST="/var/backups/app"
SRC="/home/deploy/app"
STAMP="$(date -u +%Y%m%d)"
mkdir -p "$DEST"
tar -czf "$DEST/app-$STAMP.tar.gz" -C "$(dirname "$SRC")" "$(basename "$SRC")"
find "$DEST" -name 'app-*.tar.gz' -mtime +7 -print -delete
EOF
sudo chmod 750 /usr/local/sbin/maintenance-backup.shThis is a local archive, not a real backup strategy. For restic repositories and remote copies, follow back up your Linux VPS with rsync, restic and cron and keep the same cron pattern.
Warning: A local-only archive fills the same disk as your data. If the disk is already near full, tar can fail and leave a partial file. Check free space with df -h first.
Rotate the maintenance logs with logrotate
Your scripts append to files forever, so hand those files to logrotate. Create /etc/logrotate.d/maintenance:
/var/log/maintenance/*.log {
weekly
rotate 8
compress
delaycompress
missingok
notifempty
create 0640 root adm
}Each script run opens and closes its log, so no copytruncate is needed. Test the configuration in dry-run mode, then force one real rotation:
sudo logrotate -d /etc/logrotate.d/maintenance
sudo logrotate -f /etc/logrotate.d/maintenance
ls -l /var/log/maintenance/Schedule the jobs with cron
For system-wide jobs, put a file in /etc/cron.d rather than editing root's crontab. It needs a user field and must not contain a dot in the filename — cron silently skips those.
Create /etc/cron.d/maintenance:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
30 2 * * * root flock -n /var/lock/maintenance-backup.lock /usr/local/sbin/maintenance-backup.sh
17 3 * * * root flock -n /var/lock/maintenance-cleanup.lock /usr/local/sbin/maintenance-cleanup.shThen set permissions and reload cron:
sudo chmod 644 /etc/cron.d/maintenance
sudo chown root:root /etc/cron.d/maintenance
sudo systemctl restart cron # Debian/Ubuntu
sudo systemctl restart crond # AlmaLinux/Rockyflock -n makes a run exit immediately if the previous run is still going, which prevents two backups from writing at once. Cron runs with a minimal environment, so always use absolute paths inside scripts and never rely on your interactive shell's PATH.
Tip: Keep times staggered. Two heavy jobs at the same minute spike disk I/O and can make both appear to hang.
Check it works
First confirm cron parsed the file — no output from these commands is good:
sudo systemctl status cron --no-pager | head -n 5
sudo grep -i "maintenance" /var/log/syslog # Debian/Ubuntu
sudo grep -i "maintenance" /var/log/cron # AlmaLinux/RockyYou should see a line like (root) CMD (flock -n /var/lock/maintenance-cleanup.lock...) at the scheduled minute. Then check the scripts actually produced work:
sudo tail -n 30 /var/log/maintenance/cleanup.log
sudo ls -lh /var/backups/app/If both look correct, wait one full week and confirm logrotate left a .1.gz file next to the active log.
Troubleshooting
- Script works by hand but cron produces nothing — cron's
PATHand environment differ. Use absolute paths for every command, setPATHat the top of the cron file, and check withsudo -u root -i /usr/local/sbin/your-script.sh. - Cron log shows the CMD line but no output appears — output is going to mail, and
MAILTO=""discards it. Add>> /var/log/maintenance/cron.log 2>&1to the cron line while debugging, or keep the script's own logging and remove the redirect later. - Exit code 126 or 127 — the script is not executable or the shebang is wrong. Run
ls -lon the file, thensudo chmod 750 script.shand confirm the first line is#!/usr/bin/env bash. - Two runs overlap and files collide — the job takes longer than the gap between runs. Wrap the command in
flock -nas above and check the lock path in/var/lock. - Disk still fills despite cleanup and rotation — a runaway log or many small files can exhaust inodes, not space. See fix "No space left on device" when the disk is not full.
Next steps
- Learn more cron syntax, including
@dailyand per-user crontabs, in schedule tasks with cron on Linux. - Add automatic patching with turn on automatic security updates.
- Put real off-site copies in place with back up your Linux VPS with rsync, restic and cron.
