Run a Docker command inside a Docker Cron container
The Alpine Docker image makes it very easy to build a Cron container.
When using Docker, your application typically consists of several Docker containers. Often, you want to run scripts inside these containers at certain moments, for example, every five minutes, once an hour, once a day.
This is where the job scheduler Cron comes in, and there are several options on how to do this. In this post I create a separate Cron container, and use the Docker Exec command to execute commands and scripts in another container.
I had a working solution using the Ubuntu base image but wanted a simpler setup and smaller footprint. That is why I am using the Alpine Docker image here. As always, I am running this on Ubuntu 22.04.
Alpine and Cron
Alpine already comes with Cron set up. Inside the container, there are directories where we can put our scripts:
docker run alpine:3.17 ls -l /etc/periodic
Result:
/etc/periodic:
total 20
drwxr-xr-x 2 root root 4096 Mar 29 14:45 15min
drwxr-xr-x 2 root root 4096 Mar 29 14:45 daily
drwxr-xr-x 2 root root 4096 Mar 29 14:45 hourly
drwxr-xr-x 2 root root 4096 Mar 29 14:45 monthly
drwxr-xr-x 2 root root 4096 Mar 29 14:45 weekly
Scripts in these directories, are (periodically) run by Cron as follows:
docker run alpine:3.17 crontab -l
Result:
# do daily/weekly/monthly maintenance
# min hour day month weekday command
*/15 * * * * run-parts /etc/periodic/15min
0 * * * * run-parts /etc/periodic/hourly
0 2 * * * run-parts /etc/periodic/daily
0 3 * * 6 run-parts /etc/periodic/weekly
0 5 1 * * run-parts /etc/periodic/monthly
Using Docker Volumes, we can map (mount) these directories to on our host. You probably want to run some commands or scripts at other moments, for example:
- Every minute.
- Every hour between 6 and 23.
- At midnight.
This means we must add lines to our crontab file and create new directories inside the container. We can do this in a startup file or in the Dockerfile. For example, to add a line for a directory with scripts that will be run every hour between 6 and 23 in the Dockerfile:
RUN echo "0 6-23 * * * run-parts /etc/periodic/hourly_0600_2300" >> /etc/crontabs/root
We also want to use Bash. And to be able to run Docker commands in our container that can reference other Docker containers, we add Docker to our Dockerfile and a volume mapping in our docker-compose.yml file:
RUN apk add --update --no-cache bash && \
apk add --update --no-cache docker && \
...
volumes:
# access docker containers outside this container
- /var/run/docker.sock:/var/run/docker.sock
Directories and files
Here is a tree listing of all directories and files in the project:
├── project
│ ├── cron
│ │ ├── jobs
│ │ │ ├── 15min
│ │ │ ├── 1min
│ │ │ │ └── run_every_minute
│ │ │ ├── 5min
│ │ │ │ └── run_every_5_minutes
│ │ │ ├── daily
│ │ │ ├── daily_0000
│ │ │ ├── hourly
│ │ │ │ └── run_every_hour
│ │ │ └── hourly_0600_2300
│ │ │ └── run_every_hour_0600_2300
│ │ └── Dockerfile
│ ├── docker-compose.yml
│ └── .env
For testing, I added some Bash scripts. Important: According to the Alpine documentation: 'Do not use periods on your script file names - this stops them from working.'
- run_every_minute
- run_every_5_minutes
- run_every_hour
- run_every_hour_0600_2300
The contents of all these scripts is the same:
#!/bin/bash
script_name=`basename "$0"`
echo "Running ${script_name} ..."
Make sure, that all these Bash scripts are executable, for example:
chmod a+x run_every_5_minutes
The files
Here are the other files in the project:
- Dockerfile
- docker-compose.yml
- .env
File: Dockerfile
Note that Cron must run as root, not as a user.
# Dockerfile
FROM alpine:3.17
MAINTAINER Peter Mooring peterpm@xs4all.nl peter@petermooring.com
# Install required packages
RUN apk add --update --no-cache bash && \
apk add --update --no-cache docker && \
# every minute
echo "* * * * * run-parts /etc/periodic/1min" >> /etc/crontabs/root && \
# every 5 minutes
echo "*/5 * * * * run-parts /etc/periodic/5min" >> /etc/crontabs/root && \
# every hour between 06:00 and 23:00
echo "0 6-23 * * * run-parts /etc/periodic/hourly_0600_2300" >> /etc/crontabs/root && \
# every day at 00:00
echo "0 0 * * * run-parts /etc/periodic/daily_0000" >> /etc/crontabs/root
# show all run-parts
# RUN crontab -l
WORKDIR /
File: docker-compose.yml
Note that we set the context to './cron'. In this directory are all the files associated with this container.
# docker-compose.yml
version: "3.7"
x-service_defaults: &service_defaults
env_file:
- ./.env
restart: always
logging:
driver: json-file
options:
max-size: "10m"
max-file: "5"
services:
cron:
<< : *service_defaults
build:
context: ./cron
dockerfile: Dockerfile
volumes:
- ./cron/jobs/1min:/etc/periodic/1min/:ro
- ./cron/jobs/5min:/etc/periodic/5min/:ro
- ./cron/jobs/15min:/etc/periodic/15min/:ro
- ./cron/jobs/hourly:/etc/periodic/hourly/:ro
- ./cron/jobs/hourly_0600_2300:/etc/periodic/hourly_0600_2300/:ro
- ./cron/jobs/daily:/etc/periodic/daily/:ro
- ./cron/jobs/daily_0000:/etc/periodic/daily_0000/:ro
# access docker containers outside this container
- /var/run/docker.sock:/var/run/docker.sock
command: crond -f -l 8
File: .env
In the .env file we only specify the project name:
# .env
COMPOSE_PROJECT_NAME=myapp
Building, starting and some tests
To (re)build the container:
docker-compose build --no-cache
To start the Cron container:
docker-compose up
The output will be something like:
Attaching to myapp_cron_1
cron_1 | Running run_every_minute ...
cron_1 | Running run_every_minute ...
cron_1 | Running run_every_minute ...
cron_1 | Running run_every_minute ...
cron_1 | Running run_every_minute ...
cron_1 | Running run_every_5_minutes ...
cron_1 | Running run_every_minute ...
...
The name of the Cron container is:
myapp_cron_1
To show the logs of the container:
docker logs -t -f myapp_cron_1
Result:
2023-04-18T11:52:00.871554414Z Running run_every_minute ...
2023-04-18T11:53:00.872386069Z Running run_every_minute ...
2023-04-18T11:54:00.872337812Z Running run_every_minute ...
2023-04-18T11:55:00.872792007Z Running run_every_minute ...
2023-04-18T11:55:00.872825556Z Running run_every_5_minutes ...
2023-04-18T11:56:00.875379199Z Running run_every_minute ...
To check which scripts will be run from the directory ./cron/jobs/hourly_0600_2300, run the following command in another terminal:
docker exec myapp_cron_1 run-parts --test /etc/periodic/hourly_0600_2300
This will not execute the scripts! Result:
/etc/periodic/hourly_0600_2300/run_every_hour_0600_2300
Run a script or command in another Docker container
Let's run a command in a BusyBox container. In another terminal, spin up this BusyBox container and name it 'my_busybox':
docker run -it --name my_busybox --rm busybox
Create a new script 'run_busybox_env' in the './cron/jobs/1min' directory and make it executable:
#!/bin/bash
# run_busybox_env
DOCKER=/usr/bin/docker
${DOCKER} exec my_busybox env
This script runs the 'env'-command in the 'my_busybox' container and lists the enviroment variables of the BusyBox container.
Within a minute, the result should appear in our Cron container:
cron_1 | Running run_every_minute ...
cron_1 | Running run_every_5_minutes ...
cron_1 | PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cron_1 | HOSTNAME=dc74e9704d49
cron_1 | HOME=/root
cron_1 | Running run_every_minute ...
HOSTNAME has the ContainerId of the BusyBox container, this will differ in your case. You can get the ContainerId of the BusyBox container by running:
docker ps | grep my_busybox
Summary
Using the Alpine Docker image for our Cron container was very easy. Everything worked at once. The result is less configuration and a smaller footprint.
Links / credits
Alpine Linux:FAQ
https://wiki.alpinelinux.org/wiki/Alpine_Linux:FAQ
CRON Jobs with Alpine Linux and Docker
https://lucshelton.com/blog/cron-jobs-with-alpine-linux-and-docker
Docker Tip #40: Running Cron Jobs on the Host vs. in a Container
https://nickjanetakis.com/blog/docker-tip-40-running-cron-jobs-on-the-host-vs-in-a-container
Running cron jobs in a Docker Alpine container
https://devopscell.com/cron/docker/alpine/linux/2017/10/30/run-cron-docker-alpine.html
Recent
Most viewed
- Using PyInstaller and Cython to create a Python executable
- Reducing page response times of a Flask SQLAlchemy website
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Connect to a service on a Docker host from a Docker container
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- SQLAlchemy: Using Cascade Deletes to delete related objects