Control panels

Automating server maintenance with cron jobs and shell scripting

Build a small set of shell scripts and cron jobs that handle cleanup, backups and log rotation on your Linux VPS, with locking and logging so each run is predictable.

IntermediateLinux server6 min read

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 cron on Ubuntu/Debian, systemctl status crond on 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.

Bash
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/maintenance

Every 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.

Bash
#!/usr/bin/env bash
set -Eeuo pipefail

Write 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.

Bash
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:

Bash
sudo /usr/local/sbin/maintenance-cleanup.sh
sudo tail -n 20 /var/log/maintenance/cleanup.log

Add 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:

Bash
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.sh

This 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:

INI
/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:

Bash
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.sh

Then set permissions and reload cron:

Bash
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/Rocky

flock -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:

Bash
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/Rocky

You 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:

Bash
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 PATH and environment differ. Use absolute paths for every command, set PATH at the top of the cron file, and check with sudo -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>&1 to 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 -l on the file, then sudo chmod 750 script.sh and 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 -n as 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

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.