I had a surprisingly hard time setting up some cron tasks inside of a docker container that does other things. Here are some things to note, which are also useful linux things to know.
I picked cron out of habit, because I thought all I had to do was add something like * * * * * myscript.sh
in the right location. Maybe some of the following problems could have been avoided (or replaced by others ;)) with a systemd timer.
/var/log/cron.log
. This is apparently system-dependant, so it could be somewhere else.* * * * * mycommand.sh 1>>stdout.log 2>>stderr.log
Things are all over the place:
crontab -l
will list the crons run by the current usercrontab -e
will let you edit it/etc/cron.daily
(this one has a few friends)/etc/cron.d/your-cron
(beware, those files should not have a .
, so no .sh
extension here)What should you do, what are the differences, what syntax should you use, who should own those files ? On Debian, you should read cron(8), which is well summarized here:
The main difference is that /etc/cron.d is populated with separate files, whereas crontab manages one file per user; it’s thus easier to manage the contents of /etc/cron.d using scripts (for automated installation and updates), and easier to manage crontab using an editor (for end users really). Other important differences are that not all distributions support /etc/cron.d, and that the files in /etc/cron.d have to meet a certain number of requirements (beyond being valid cron jobs): they must be owned by root, and must conform to run-parts’ naming conventions (no dots, only letters, digits, underscores, and hyphens). If you’re considering using /etc/cron.d, it’s usually worth considering one of /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly, or /etc/cron.monthly instead.
cron
does not load the environment…not even PATH, so your command may crash because the binary is not found without the absolute path.
You have to provide the environment you need somehow. Some solutions:
/etc/crontab
. Because, environment may not be inherited:
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
bash -l
: Creates a login shell, that loads the environment when you run the command. It can be done with #!/bin/bash -l
at the beginning of the script/etc/environment
or a .profile
. /etc/environment
is a system-wide configuration file (used by all users), .profile
is for one user onlyenv >> /etc/environment
and all the current environment variables are now available in the CRON jobs.Docker is meant to run only one program, so in theory you should not run both a program and cron. A solution is to use a Docker entrypoint
at the end of your Dockerfile
:
COPY your-cron-task /etc/cron.d/your-cron-task
## a custom entry point − needed by cron
COPY config/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD the_command_you_actually_want_to_run
and entrypoint.sh
:
#!/bin/bash
# We want the environment to be available to CRON
# https://stackoverflow.com/questions/27771781/how-can-i-access-docker-set-environment-variables-from-a-cron-job
printenv | grep -v "no_proxy" >> /etc/environment
# When we run your app, we also want to start the cron service
crontab /etc/cron.d/your-cron-task
service cron start
# Hand off to the CMD
exec "$@"
You may not want to start off a new copy of your script before the old one has finished. A lock seems to be a solution:
#!/bin/bash
LOCKFILE="/var/lock/`basename $0`"
(
flock -n 9 || {
echo “$0 already running”
exit 1
}
# The command you actually want to run
) 9>$LOCKFILE
Not critical and not that hard, but:
Fugit::Nat.parse('every day at five') => '0 5 * * *'