angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Run a Docker command inside a Docker Cron container

The Alpine Docker image makes it very easy to build a Cron container.

18 April 2023 Updated 19 April 2023
In Docker
post main image
https://unsplash.com/@unsplash

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

Leave a comment

Comment anonymously or log in to comment.

Comments

Leave a reply

Reply anonymously or log in to reply.