Cron jobs in Bash for DevOps
In the previous article we managed variables, .env files, CLI arguments, and special Bash variables. Once a script works reliably when you run it manually, the next question is: how do you make it run at the right time automatically?
cron is the classic Linux/Unix tool for running commands or scripts on a schedule. In DevOps, cron is often used for periodic backups, log cleanup, health checks, file sync, reports, SSL certificate checks, or lightweight maintenance endpoints.
The important detail is that cron runs with a much smaller environment than your interactive terminal. A script that works manually may still fail in cron if it depends on PATH, the current directory, environment variables, or clear logging.
What are cron and crontab?
cron is a background daemon that reads schedule tables and runs commands when their time matches. Those schedule tables are commonly called crontab files.
Basic commands:
| |
You usually edit your own schedule with crontab -e. Be careful with crontab -r on production servers because it removes all jobs for that user.
A simple cron entry:
| |
This runs the script every five minutes and appends both stdout and stderr to a log file.
The five-field schedule syntax
A common cron job has five time fields followed by the command:
| |
Common examples:
| |
Cron also supports lists, ranges, and step values:
| |
For production schedules, add a comment above each job so the next operator understands why it exists.
Shortcuts: @daily, @hourly, @reboot
Many cron implementations support shortcuts that make crontab easier to read:
| |
@daily usually means once per day at midnight. @reboot runs when the cron daemon starts and is useful for small boot-time tasks.
Do not overuse @reboot for important long-running services. A systemd service is usually better for daemons because it provides restart policies, dependencies, and clear logs through journalctl.
Write cron-friendly Bash scripts
A script executed by cron should prepare its own environment instead of depending on an interactive shell.
A good starter structure:
| |
Key points:
- Use a clear shebang.
- Enable
set -euo pipefailso failures are not silently ignored. - Define
PATHin the script or in crontab. - Use absolute paths for important files.
- Log with timestamps.
- Do not assume cron runs from your project directory.
Redirect logs in cron
Cron can send output through local mail if the system is configured for it. In real DevOps work, writing to log files is often easier to control.
Redirect stdout and stderr:
| |
Meaning:
>> /var/log/check-health.log: append stdout to the file.2>&1: send stderr to the same destination as stdout.
If you want separate error logs:
| |
For frequent jobs, make sure you rotate or limit logs. Otherwise a log file can eventually fill the disk.
Check cron logs
Cron log locations depend on the distribution and logging setup.
On many Ubuntu/Debian systems using syslog:
| |
On systems using the systemd journal:
| |
Some distributions name the service crond instead of cron. If you are not sure, check both:
| |
Remember that cron logs usually tell you whether cron started the command. They do not prove that the command completed successfully. For that, redirect output or write logs inside the script.
PATH and environment problems in cron
Cron does not load your full .bashrc, .profile, or terminal environment. This is a common reason why a script works manually but fails in cron with command not found.
Define PATH at the top of crontab:
| |
Or use absolute paths in the script:
| |
For app-specific variables, prefer loading a config file you control:
| |
Avoid putting secrets directly in crontab if multiple people can read crontabs or system backups. For important secrets, use a secret manager, a tightly permissioned file, or the secret mechanism of your platform.
DevOps practice: nightly database backup
The example below shows a nightly PostgreSQL backup script. It loads variables from a config file, creates a backup directory, compresses the dump, and deletes old backups.
File /opt/scripts/backup-db.sh:
| |
File /etc/example-app/backup.env:
| |
Set strict permissions:
| |
Add a cron job that runs every day at 02:30:
| |
In a real environment, test restores regularly. A backup is only useful when you know it can be restored.
DevOps practice: healthcheck every five minutes
This script checks a /health endpoint, retries a few times, and writes logs. If the endpoint still fails, the script exits with a non-zero code so the failure appears in logs.
File /opt/scripts/check-health.sh:
| |
Crontab:
| |
An inline environment variable is fine for non-sensitive values. If you need a token or secret header, load it from a tightly permissioned file or a secret manager instead of putting it directly in crontab.
Avoid overlapping jobs
If a job runs longer than its schedule interval, the next run can start before the previous run finishes. This is risky for backups, cleanup tasks, and deployments.
A common solution is flock:
| |
flock -n does not wait for the lock. If the previous job is still running, the new one exits immediately. For backup:
| |
Not every system has flock installed, but on many Linux distributions it comes from util-linux. If your environment does not have it, use another locking mechanism and handle stale locks carefully.
Common mistakes
- Forgetting log redirection: The job fails but you have no output to debug.
- Depending on the working directory: Cron does not guarantee your project directory.
- Missing
PATH: Commands such asdocker,kubectl,node, orpythonmay not be found. - Using relative paths: Config, log, and backup files should use absolute paths.
- Allowing overlapping runs: Backup or cleanup jobs may run concurrently and damage data.
- Putting secrets directly in crontab: This is hard to manage, audit, and protect.
- Not testing the command before saving crontab: Run the script as the same user that cron will use.
Implementation notes
- When applying this to your project, standardize each cron job with a small checklist:
- The script has a shebang,
set -euo pipefail, and absolute paths. - The crontab includes a comment explaining the job.
- Output is redirected to a log file or centralized logging system.
- Long-running jobs use a lock to avoid overlap.
- Secrets live in an appropriate place, not hardcoded in public scripts.
- The script has a shebang,
- Best practices:
- Test the script manually as the correct user:
sudo -u <app-user> /opt/scripts/job.sh. - Include timestamps in logs for incident analysis.
- Define
PATHclearly in the script or crontab. - Use
flockfor jobs that may run longer than expected. - Watch log and backup size with
logrotateor a retention policy.
- Test the script manually as the correct user:
- Troubleshooting:
- Job not running? β Check
crontab -l, schedule syntax, timezone, and cron logs. - Job runs but the command fails? β Check the script’s own log file.
- Works manually but fails in cron? β Check
PATH, working directory, permissions, and environment variables.
- Job not running? β Check
π― Conclusion
Cron turns Bash scripts into scheduled operational automation: nightly backups, periodic health checks, log cleanup, and reports. To make cron jobs reliable, write scripts that control their own environment, use absolute paths, log clearly, and prevent overlapping runs when jobs may take longer than expected.
In the next article, we will debug Bash scripts with bash -x, bash -n, set -x, set -e, set -u, pipefail, trap ERR, PS4, and shellcheck so problems become easier to find. π
References
- man7.org β crontab(5) β crontab syntax, the five time fields, and cron environment variables.
- man7.org β cron(8) β cron daemon behavior and schedule processing.
- man7.org β flock(1) β using
flockto avoid overlapping jobs. - PostgreSQL Documentation β pg_dump β official documentation for
pg_dumpused in the backup example. - curl Documentation β reference for
--fail,--silent,--show-error, and--max-timeused in the healthcheck example.
