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:
- every minute
- every 15 minutes
- every 30 minutes
- hourly
- every 12 hours
- daily
- weekly
- monthly
To run scripts on any of these time intervals, copy the script(s) into the associated time directories:
- /etc/periodic/1min/
- /etc/periodic/15min/
- /etc/periodic/30min/
- /etc/periodic/hourly/
- /etc/periodic/12hour/
- /etc/periodic/daily/
- /etc/periodic/weekly/
- /etc/periodic/monthly/
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.