Dale M. Picou, Jr.

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 did take a look at Jobber but stuck with cron. The decision for me was as follows: cron familiarity, cron image has a smaller footprint, and did not need any of the additional features Jobber supplied.

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-RPM 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 strait 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 3 different tags with 2 different variants: standard, and default. The latest variant is the same as the standard image.

standard
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.
default
The default 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.

A note on the default image and why I included this variant. Through building multiple applications, I found was easier for me 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:

<?php
  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.

FROM alpine:3.13.6

# 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 default variant uses the standard image as the base and copies in FastCGI shell script into the 1min crontab directory.

FROM djpic/cron: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.

#!/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

Finally the build script. This script is what I use to build the images and upload them to DockerHub. The build script starts by removing any current versions on the build machine. The next step is to create the standard image then tag it standard and latest. Following building the standard image, the script continues and builds the default image.

After all variants are built, the script finishes by pushing all the images to Dockerhub and removes them from the build machine.

#!/bin/bash

# Remove stale images
docker rmi djpic/cron:latest
docker rmi djpic/cron:default
docker rmi djpic/cron:standard

# Build standard cron image
docker build --tag djpic/cron:standard .
docker tag djpic/cron:standard djpic/cron:latest

# Build default cron image
cd default
docker build --tag djpic/cron:default .

# Push images to Dockerhub
docker push djpic/cron:latest
docker push djpic/cron:standard
docker push djpic/cron:default

# remove images
docker rmi djpic/cron:default
docker rmi djpic/cron:standard
docker rmi djpic/cron:latest

Sample docker-compose file

This image, like all my other images, are designed to work with each other. So for this, I have provided a sample docker-compose file which includes djpic/nginx and djpic/php. I do use a custom image of Traefik, so this docker-compose file does include djpic/traefik labels.

version: "3.2"
services:
  nginx:
    image: djpic/nginx:phpfpm
    networks:
      - FrontEnd
      - 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:default
    networks:
      - BackEnd

networks:
  FrontEnd:
    external: true
  BackEnd:

Conclusion

This djpic/cron image rounds out my development. I hope you took something from this article and I highly recommend you take a look at Jobber then decided what works best for your development. It is all about what works great for your workflow; cron is just my preference at this time. All the Dockerfiles, scripts, and supporting files to build these image 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. Like stated in the article, all of this images are designed to work together.

Follow my twitter account @djpic_llc for updates to the 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.