When you need to setup a cron job, you can do it using Docker. The best way to do it with Docker is using an Alpine Linux image, as it has only 5MB initial size.
Don’t know what is Docker? Check more about it here.
In this post I expect you to already have Docker and docker-compose installed on your OS.
Creating the image
In the case below I needed an Alpine Linux container with Node.js inside, as I wanted cron to run a Node.js script.
First we create a file called Dockerfile with the following content (yes, with no file extension):
1 2 3 4 5 6 7 8 9 FROM node:8-alpine MAINTAINER Jon Ribeiro <[email protected]> RUN apk update && apk add tzdata &&\ cp /usr/share/zoneinfo/Europe/Prague /etc/localtime &&\ echo "Europe/Prague" > /etc/timezone &&\ apk del tzdata && rm -rf /var/cache/apk/* CMD chown root:root /etc/crontabs/root && /usr/sbin/crond -f
Let’s save this file inside a directory called cron.
The FROM command tells Docker which image we want to use for building our image.
The MAINTAINER command tells Docker who is maintaining this Dockerfile.
The RUN command tells Docker which commands to run on image creation.
It’s very important for your Dockerfile to have the least number of commands possible, as another RUN command for example, would create another layer in the resulting image. That’s why we concatenate the commands with && instead of writing a RUN command for each of them. Simplicity should be the goal.
In the example, I set the timezone to Prague (my current timezone), but you can change it to yours instead, of course.
The CMD command tells Docker what will run inside the container that will be created from this image, which is cron in this case. Exactly what we need.
Now we need to configure what this image will run using cron.
I created this silly example below just to illustrate what could be done.
Save the content below to a file called root in the same directory where you saved the Dockerfile (the cron directory you created before).
1 2 3 4 5 6 7 8 9 10 11 # ┌───────────── minute (0 - 59) # │ ┌───────────── hour (0 - 23) # │ │ ┌───────────── day of month (1 - 31) # │ │ │ ┌───────────── month (1 - 12) # │ │ │ │ ┌───────────── day of week (0 - 6) (Sunday to Saturday; # │ │ │ │ │ 7 is also Sunday on some systems) # │ │ │ │ │ # │ │ │ │ │ # * * * * * command to execute 0 7,19 * * * /usr/local/bin/node /home/node/hello.js >> /var/log/crontest/hello.log 2>&1
Now create a directory called scripts on the same level of the cron directory, not inside of it.
Inside this scripts directory, create our hello.js script with the following content:
Ok, so how to add this to our container?
I like using docker-compose as it enables me to orchestrate many containers in a simple way, using only one docker-compose.yml file.
With docker-compose you can for example create a file with all the containers you need and then run docker-compose up for it to start all of them at once, instead of dealing with docker run and passing all parameters every time, etc.
Let’s create a file called docker-compose.yml with the following content in same level as our scripts and cron directories:
1 2 3 4 5 6 7 8 9 version: "2" services: cron: build: cron container_name: crontest volumes: - /var/log/crontest:/var/log/crontest - cron/root:/etc/crontabs/root - sripts:/home/node
Spacing is very important here. Visual Studio Code can help you with that, as it detects yml files.
The cron directory is informed in the build atribute there, so docker-compose will build the image using the Dockerfile we created inside the cron directory.
In “volumes”, the crontest directory inside your host OS is being mounted inside the container using the same path there, so when our script outputs to hello.log as configured above, the log will be easily accessible from the host OS instead of having to enter the cron container to read it. If this directory doesn’t exist on your host OS, docker-compose will create it.
For the next lines configured inside “volumes”, the root file we created inside the cront directory is being mounted inside the container in the /etc/crontabs/root path. On the 3rd line, everything from the scripts directory is being mounted inside /home/node inside the container, as this is the home directory this Node.js container comes with.
Instead of mounting these files inside the container using docker-compose, you could have added them on image creation using the ADD command, but then you’d have to rebuild the image if you needed to change any of these files and the resulting image would also have more layers. Because of that, I prefer to mount them from the host OS instead, so if I need to modify any of these files, they are not builtin with the image in any way.
After saving the docker-compose.yml file, you can run the command to build our docker image and start our container from it:
This way all the commands will show up on your terminal and you won’t be able to use this terminal for anything else. If you add -d in the end of the command, then it runs as a daemon and your terminal will be free, but you wont see any output about the image build process or when it’s running.
If you run docker-compose witout the daemon option, you just need to use Ctrl + c when you want to stop the containers specified in your docker-compose.yml file. If you run as a daemon, you can run docker-compose stop to stop the containers and then if you want to remove them you can run docker-compose rm
In order to list the current containers:
docker ps -a
And the images:
Running on startup
After creating the image and checking that everything works as expected, we need to create a startup script so if our OS gets restarted the cron container runs automatically.
The servers I configure are usually running Debian Linux, my personal notebook runs Arch Linux and at work I run Manjaro. All these Linux distributions come with systemd, so I’ll show an example of how the startup could be configured for that.
Let’s save a file called docker-infra.service inside /etc/systemd/system/ with the following content:
1 2 3 4 5 6 7 8 9 10 11 12 [Unit] Description=Docker infra Requires=docker.service After=docker.service [Service] Restart=always ExecStart=/usr/local/bin/docker-compose -f /home/jon/crontest/docker-compose.yml up ExecStop=/usr/local/bin/docker-compose -f /home/jon/crontest/docker-compose.yml stop [Install] WantedBy=default.target
You need to change the path to your docker-compose.yml file to the one where you saved yours inside your server, of course.
Then let’s reload the system daemons:
sudo systemctl daemon-reload
and enable our newly created daemon to run on system startup:
sudo systemctl enable docker-infra
If you need to stop or check the status of the daemon that controls our docker container, just change the enable in the systemctl command above to stop or status and that’s it.
In this post we saw how to create a Docker image to use as cron to run Node.js scripts. Then we saw how to use docker-compose and create a daemon for it to run on system startup.
If you have any suggestion or doubt, let me know in the comments below :)