Docker Image: Alpine CRON

Introduction

Cron is a service that has been around for some time. For anyone unfamiliar, the service is used to run scheduled scripts/commands in Linux/Unix environments. My use case for cron is application and/or database maintenance.

When making a switch to Docker, the use case was still there, but I could not find a simple cron enabled container. So I built one: djpic/cron

I originally took a look at Jobber but stuck with cron. The decision for me was as follows: cron familiarity, cron image has a smaller footprint, and I did not need any of the additional features Jobber supplied. And the largest reason, Jobber is no longer being maintained.

What is the image?

This Docker image is built from the official Alpine image. Cron service is already included in the Alpine image, however, a mechanism to run Hypertext Preprocessor (PHP) scripts on another PHP-FPM enabled container is not.

To combat this, FastCGI is also installed in this image. FastCGI is a binary protocol for interfacing interactive programs with a web server. In short, it allows direct execution of PHP scripts running in a PHP-FPM container such as djpic/php.

How to use the image

Using the image is straight forward. Included is a crontab file that controls when jobs are run. The crontab file will execute items in specific folders in the following intervals:

To run scripts on any of these time intervals, copy the script(s) into the associated time directories:

Make sure the scripts have the correct execute permissions (i.e. chmod 755).

Image Tags

There are a total of three different tags with two different variants: standard, and default. The latest variant is the same as the standard image.

standard/latest
Standard it just that, the standard version. Jobs will run based on the intervals and directories stated in the how to use the image section above. FastCGI is installed in this version for any PHP scripts that need to run.
PHP
The PHP variant is built from the standard variant but already includes a script which runs every minute. This script uses FastCGI to execute a PHP file located at /app/private/scheduled_jobs/1min.php on the PHP-PFM container.

Why include a PHP variant? Through building multiple applications, I found it easier to have cron run a script every minute. Then use the PHP script to control what runs when. For example, this PHP will execute a task daily at 2am:

if (date('Hi') === '0200') {
  require_once('script_to_execute.php');
}

How the image is built

The build starts with Dockerfile for the standard variant. This Dockerfile will install FastCGI, create the additional crontab directories, copy the customized crontab file, and set the entry point to run cron as a daemon.

ARG alpine_version

FROM alpine:${alpine_version}

# Install fastCGI to execute PHP scripts in PHP-FPM container
RUN apk update \
    && apk add fcgi

# Create Crontab directories
RUN mkdir /etc/periodic/1min \
    && mkdir /etc/periodic/30min \
    && mkdir /etc/periodic/12hour

# Copy in customized crontab file
COPY crontab /etc/crontabs/root

# Copy in entrypoint.sh script; this allows cron to run as daemon
COPY entrypoint.sh /entrypoint.sh
RUN chmod 755 /entrypoint.sh
ENTRYPOINT /entrypoint.sh

The customized crontab file is shown here:

# do daily/weekly/monthly maintenance
# min	hour	day	month	weekday	command
*	*	*	*	*	run-parts /etc/periodic/1min/
*/15	*	*	*	*	run-parts /etc/periodic/15min/
*/30	*	*	*	*	run-parts /etc/periodic/30min/
0	*	*	*	*	run-parts /etc/periodic/hourly/
0	*/12	*	*	*	run-parts /etc/periodic/12hour/
0	2	*	*	*	run-parts /etc/periodic/daily/
0	3	*	*	6	run-parts /etc/periodic/weekly/
0	5	1	*	*	run-parts /etc/periodic/monthly/

And entrypoint shell script which forces cron to run as a daemon:

#!/bin/sh

echo "For more information, visit https://hub.docker.com/repository/docker/djpic/cron"
echo "Starting DockerContainer..."

crond -f -l 8 -d 8 -L /dev/stdout

The Dockerfile for the PHP variant uses the standard image as the base and copies in FastCGI shell script into the 1min crontab directory.

ARG alpine_version

FROM djpic/cron:${alpine_version}-standard

# Copy FastCGI script to run every minute
COPY script.sh /etc/periodic/1min/default

# Update permissions on FastCGI script
RUN chmod 755 /etc/periodic/1min/default

The FastCGI shell script sets the variables needed for FastCGI to execute the 1min.php file. This script assumes the file is located at /app/private/scheduled_jobs/1min.php. You can use this FastCGI shell script as an example for any customization you would like to do. This script also assumes the linked PHP container is named php.

#!/bin/sh
echo "Running default command...." > /dev/stdout
SCRIPT_NAME=/app/private/scheduled_jobs/1min.php \
SCRIPT_FILENAME=/app/private/scheduled_jobs/1min.php \
REQUEST_METHOD=GET \
cgi-fcgi -bind -connect php:9000

Next is the build script. The script is executed to build all variants of the image and upload them to DockerHub and GitLab. The alpine_version is passed to the script via command-line argument. The script builds and tags the standard/latest variant first then the PHP variant is built.

#!/bin/bash

# What base image of Alpine to use for all builds
alpine_version=$1


if [[ -v alpine_version ]]; then
  # Build standard cron image
  docker build --build-arg alpine_version=$alpine_version --tag djpic/cron:$alpine_version-standard --tag $CI_REGISTRY_IMAGE/cron:$alpine_version-standard .
  docker tag djpic/cron:$alpine_version-standard djpic/cron:latest
  docker tag $CI_REGISTRY_IMAGE/cron:$alpine_version-standard $CI_REGISTRY_IMAGE/cron:latest


  # Build php cron image
  cd php
  docker build --build-arg alpine_version=$alpine_version --tag djpic/cron:$alpine_version-php --tag $CI_REGISTRY_IMAGE/cron:$alpine_version-standard .

  # Build development cron image
  cd ../development/
  docker build --build-arg alpine_version=$alpine_version --tag djpic/cron:$alpine_version-dev --tag $CI_REGISTRY_IMAGE/cron:$alpine_version-dev .
  docker tag djpic/cron:$alpine_version-dev djpic/cron:dev
  docker tag $CI_REGISTRY_IMAGE/cron:$alpine_version-dev $CI_REGISTRY_IMAGE/cron:dev

  # Push images to Dockerhub
  docker push djpic/cron:latest
  docker push djpic/cron:dev
  docker push djpic/cron:php
  docker push djpic/cron:$alpine_version-standard
  docker push djpic/cron:$alpine_version-php
  docker push djpic/cron:$alpine_version-dev

  docker push $CI_REGISTRY_IMAGE/cron:latest
  docker push $CI_REGISTRY_IMAGE/cron:dev
  docker push $CI_REGISTRY_IMAGE/cron:php
  docker push $CI_REGISTRY_IMAGE/cron:$alpine_version-standard
  docker push $CI_REGISTRY_IMAGE/cron:$alpine_version-php
  docker push $CI_REGISTRY_IMAGE/cron:$alpine_version-dev

else
  echo "Alpine Version Missing"
  exit 1
fi

The final piece will be the gitlab-ci.yml file. The gitlab ci file has separate jobs for each version of the base alpine image which allows the jobs to run in parallel. Each job executes in a docker-in-docker (DinD) container making cleanup easy.

Using the before scripts, bash is installed, both DockerHub and GitLab registries are logged into, and the build.sh script is granted the necessary permissions. Within the script section, the build.sh script is executed passing the alpine version via the command-line argument.

# Gitlab CI to build docker images
stages:
  - build
  - release

services:
  - docker:dind

before_script:
  - apk update
  - apk add bash
  - docker login --username $dockerhub_username --password $dockerhub_password
  - docker login --username $CI_REGISTRY_USER --password $CI_REGISTRY_PASSWORD $CI_REGISTRY
  - chmod 755 build.sh

alpine-3.21:
  stage: build
  tags:
    - DockerExe

  script:
    - ./build.sh 3.21


include:
  # Only Run the production CI when a production tag is added.
  - local: .release.gitlab-ci.yml
    rules:
      - if: '$CI_COMMIT_TAG =~ /^prod-.*$/'
        when: always

Sample docker-compose file

This image is specifically crafted to complement and integrate seamlessly with my existing djpic/nginx, djpic/php, and djpic/traefik environment. For example, below is a sample docker-compose file which includes djpic/nginx and djpic/php.

There is a custom Traefik image that is also available, and this docker compose file includes djpic/traefik labels.

project_name: "samplecomposefile"
services:
  nginx:
    image: djpic/nginx:phpfpm
    networks:
      - Traefik
      - BackEnd
    depends_on:
      - php
    volumes:
      - ./application/:/app
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.djpicdemo-web.rule=Host(`demo.djpic.net`)"
      - "traefik.http.routers.djpicdemo-web.entrypoints=web"
      - "traefik.http.routers.djpicdemo-web.middlewares=https-redirect@file"
      - "traefik.http.routers.djpicdemo-tls.rule=Host(`demo.djpic.net`)"
      - "traefik.http.routers.djpicdemo-tls.entrypoints=websecure"
      - "traefik.http.routers.djpicdemo-tls.tls.certresolver=letsencrypt"
      - "traefik.http.routers.djpicdemo-tls.middlewares=secure-headers@file, compress-content@file"
    restart: always

  php:
    image: djpic/php:mysqli
    networks:
      - BackEnd
    volumes:
      - ./application/:/app

  memcached:
    image: library/memcached:alpine
    networks:
      - BackEnd
    command: ["-m", "64m"]
    restart: always

  cron:
    image: djpic/cron:php
    networks:
      - BackEnd

networks:
  Traefik:
    external: true
  BackEnd:

Conclusion

This djpic/cron image rounds out my development architecture. I hope you took something from this article. All the Dockerfiles, scripts, and supporting files to build these images are available on Gitlab.

Also, be sure to check out my other Dockerhub images and their associated articles: djpic/nginx, djpic/php, and djpic/traefik. All of these images are designed to work together.

Follow my twitter account @djpic_llc for updates to this article and other announcements. I also welcome all constructive input. If anything is incorrect or needs further explanation, feel free to contact me.

Disclaimer:
The contents of this article are of my opinion and based on my experience. Before applying any of the concepts or suggestions in this article, complete your own independent research and testing. By reading this article, the reader agrees that Dale M. Picou, Jr. shall not be held liable for any negative impact when applying these tutorials, concepts, and/or suggestions.