Skip to content
DevOps devops shell 6 min read

Automating Scripts with cron

Writing a shell script is only half the job. The real payoff comes when that script runs on its own, every night, without you logging in. That is what cron (a background service that runs commands on a schedule) is for. In this page we will take a real backup script, wire it into your crontab (your personal list of scheduled jobs), capture its output to a log file, and walk through the silent traps that catch almost everyone the first time.

When to use cron (and when not to)

Cron is the right tool when you have a repeating task on a fixed clock: nightly database backups, hourly log cleanup, a health check every five minutes. It is simple, built into every Ubuntu server, and needs no extra software.

It is the wrong tool when you need a task to run because something happened (a file arrived, a web request came in) rather than at a fixed time — that is event-driven work, better handled by systemd path units or a queue. It is also a poor fit for jobs that must keep precise state between runs or restart on failure; for those, prefer a systemd timer (a scheduler built into systemd, Ubuntu’s service manager). For most “run this script every night” needs, cron is perfect.

This page focuses on writing and scheduling the script. For the deeper mechanics of the cron daemon, the five-field time syntax, and /etc/cron.d, see the cron admin page in the Linux administration docs.

A real script worth scheduling

Let’s start with a script that backs up a PostgreSQL database and deletes backups older than seven days. Save it at /usr/local/bin/db-backup.sh.

#!/usr/bin/env bash
set -euo pipefail

# --- configuration ---
BACKUP_DIR="/var/backups/postgres"
DB_NAME="appdb"
DB_USER="postgres"
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
OUTFILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.sql.gz"

mkdir -p "$BACKUP_DIR"

echo "[$(date '+%F %T')] starting backup of ${DB_NAME}"

# absolute paths: cron has a minimal PATH (see below)
/usr/bin/pg_dump -U "$DB_USER" "$DB_NAME" | /usr/bin/gzip > "$OUTFILE"

echo "[$(date '+%F %T')] wrote ${OUTFILE}"

# delete backups older than 7 days
/usr/bin/find "$BACKUP_DIR" -name '*.sql.gz' -mtime +7 -delete

echo "[$(date '+%F %T')] cleanup done"

The set -euo pipefail line is important for scheduled scripts: -e stops on the first error, -u treats unset variables as errors, and -o pipefail makes the whole pipeline fail if pg_dump fails even though gzip succeeds. Without it, a broken backup could silently write an empty file.

Make it executable and test it once by hand before scheduling:

sudo install -m 0755 db-backup.sh /usr/local/bin/db-backup.sh
sudo -u postgres /usr/local/bin/db-backup.sh

Output:

[2026-06-15 21:30:01] starting backup of appdb
[2026-06-15 21:30:03] wrote /var/backups/postgres/appdb_2026-06-15_21-30-01.sql.gz
[2026-06-15 21:30:03] cleanup done

If it works when you run it yourself, you are ready to schedule it.

Wiring it into crontab

Each user has their own crontab. Open it with the -e (edit) flag:

crontab -e

The first time, Ubuntu asks which editor to use — pick nano if you are unsure. Add this line to run the backup every day at 02:30 and append all output to a log:

30 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1

The five fields before the command are minute, hour, day-of-month, month, day-of-week. So 30 2 * * * means “at minute 30 of hour 2, every day”. Save and exit; cron picks up the change immediately.

Confirm your jobs are installed with -l (list):

crontab -l

Output:

30 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1

Capturing output to a log

Cron does not show output on a screen — there is no screen. By default it tries to email anything a job prints, which on a fresh server usually goes nowhere. So you must redirect output yourself. Two operators do the work:

SymbolMeaningUse it for
>>Append standard output (normal text) to a fileKeeping a running log
2>&1Send standard error (error messages) to the same place as standard outputCapturing failures too

The order matters: >> file 2>&1 first points stdout at the file, then points stderr at “wherever stdout goes”. Writing 2>&1 >> file does not work the same way. Always put 2>&1 last.

To stop the log growing forever, install a logrotate rule at /etc/logrotate.d/db-backup:

/var/log/db-backup.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
}

This keeps eight weeks of compressed logs and quietly does nothing if the file is missing.

The cron environment pitfalls

This is where good scripts break. Cron runs your job in a stripped-down environment that looks nothing like your login shell.

PATH is tiny. Your interactive shell has a rich PATH (the list of folders searched for commands). Cron typically only has /usr/bin:/bin. So pg_dump might be found by hand but “command not found” under cron. There are two fixes — use whichever you prefer:

# Option A: set PATH at the top of the crontab (applies to all jobs)
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
30 2 * * * /usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1

Or, more robustly, call every external command by its absolute path inside the script (as the example above does with /usr/bin/pg_dump). Find the real path with which:

which pg_dump gzip find

Output:

/usr/bin/pg_dump
/usr/bin/gzip
/usr/bin/find

The working directory is your home folder, not where the script lives. Any relative path like ./config is gambling. Always use absolute paths for files, or cd to a known directory first.

Most of your environment variables are gone. Variables you set in ~/.bashrc are not loaded, because cron does not start a login shell. Define what your script needs inside the script or at the top of the crontab.

Never store secrets like database passwords directly in the crontab — crontab -l and process listings can expose them. Put credentials in a file readable only by the job’s user, for example a ~/.pgpass file with chmod 600, and let the tool read them from there.

Verifying it actually ran

After the scheduled time passes, check two things. First, the cron service log:

grep CRON /var/log/syslog | tail -5

Output:

Jun 15 02:30:01 web01 CRON[20451]: (ubuntu) CMD (/usr/local/bin/db-backup.sh >> /var/log/db-backup.log 2>&1)

Then your own log file to confirm the script succeeded, not just that cron tried:

tail -3 /var/log/db-backup.log

A line in syslog only means cron launched the job. Your own log tells you whether it worked.

Best Practices

  • Always test the script by hand first, ideally as the same user cron will use, before adding it to the crontab.
  • Use absolute paths for both the script and every command it calls — never rely on cron’s PATH.
  • Redirect output with >> logfile 2>&1 so you capture both normal output and errors; cron’s email delivery is unreliable.
  • Add set -euo pipefail so a failing step stops the script instead of producing a broken, empty result.
  • Keep secrets out of the crontab; read them from a chmod 600 file owned by the job’s user.
  • Rotate your log files with logrotate so a chatty job does not slowly fill the disk.
  • Use a comment line in your crontab above each job explaining what it does and why, so future-you understands it.
Last updated June 15, 2026
Was this helpful?