Save the output to a log file
Cron logs only that a job started, never what it printed or its exit code. Redirect stdout and stderr yourself. Two arrows append, one arrow overwrites on every run.
*/5 * * * * /home/crons/send-updates.py >> /home/crons/send-updates.log 2>&1
Stop a job overlapping itself
Cron starts the next run on schedule even if the last one is still going, and a slow day can pile up dozens of copies. flock takes a lock file first and, with -n, skips the run when the lock is held.
*/5 * * * * flock -n /tmp/send-updates.lock /home/crons/send-updates.py
Set environment variables
Declare them on their own lines above the jobs, without export. For a value that differs per job, put it inline before the command. For many variables, source a file first.
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""
0 * * * * APP_ENV=production /srv/app/export.py
0 0 * * * APP_ENV=staging /srv/app/export.py
0 3 * * * . /etc/app.env; /srv/app/nightly.sh
Run from the right directory
Jobs start in the owner's home directory. If a script relies on relative paths, change into its directory first, or put its directory on PATH.
0 * * * * cd /srv/app && ./job.sh
Run a job as another user
Use a system crontab, which has a user column. A file in /etc/cron.d keeps each app's jobs together. Note that sudo crontab -e edits root's own crontab, which has no user column.
# /etc/cron.d/database-backup
0 0 * * * dataproc /srv/bin/database-backup.sh
Run every 30 seconds
Cron cannot go below one minute. Schedule a wrapper every minute that runs the job twice with a 30 second sleep, and lock it so copies cannot stack up. A systemd timer with OnCalendar=*-*-* *:*:00,30 does this natively.
#!/bin/bash
for i in 1 2; do
/home/crons/send-updates.py &
sleep 30
done
# crontab: * * * * * flock -n /tmp/wrapper.lock /home/crons/wrapper.sh
Run a command the way cron does
Reproduce a failure by running as the right user, with /bin/sh, from the home directory and with a clean environment. Paste the command exactly as it appears after the schedule.
sudo -u <user> /bin/sh -c "cd ~ && <command from the crontab>"
# or a fully empty environment:
env -i /bin/sh
Check whether a job is running
grep the process list for the command. More than one line means the job is hanging, is scheduled twice, or takes longer than its interval. The second command lists every process cron started.
ps aux | grep "/home/crons/send-updates.py" | grep -wv grep
ps h --ppid $(pgrep cron)
Cron with Docker
Either keep the schedule on the host and run the job inside the app image, or build a small cron container that installs cron and copies a crontab into /etc/cron.d. Base the second image on the app image so it shares dependencies and secrets.
# host crontab
0 8 * * * docker run --rm acme:app /src/scripts/send-coupon-mail.sh
# Dockerfile for a cron container
RUN apt-get update && apt-get install -y cron
COPY Crontab /etc/cron.d/app
CMD cron && tail -f /var/log/cron.log
Prove cron itself works
When nothing else helps, schedule the simplest possible job every minute and tail the file. Once that ticks, swap in your command and see what changes.
* * * * * date >> /tmp/cronlog