There is nothing easier than adding
* * * * * cd /your-project && php artisan schedule:run
to crontab according to Laravel Docs, right?
Have you tried to containerize it with Docker? You probably have. Traditional approach is to just apt install cron
and add crontab entry to some /etc/cron.d/app
. It will work, but there are some issues with this approach:
- main cron process runs on
root
user - often there are problems with missing logs
We will address those issues starting with minimal Dockerfile
with some good Docker practices included:
# syntax=docker/dockerfile:1
# Normally you inherit from your Laravel app image
FROM php:8.3-fpm AS cron
# Use 'root' user only when you need to
USER root
# Install dependencies, make sure to add '--no-install-recommends' flag
RUN <<EOT bash
set -e
apt update
apt install -y curl --no-install-recommends
rm -rf /var/lib/apt/lists/*
EOT
# Latest releases available at https://github.com/aptible/supercronic/releases
ARG SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.29
ARG SUPERCRONIC=supercronic-linux-amd64
ARG SUPERCRONIC_SHA1SUM=cd48d45c4b10f3f0bfdd3a57d054cd05ac96812b
# Install Supercronic instead of typical 'apt install cron'
RUN <<EOT bash
set -e
curl -fsSLO "{$SUPERCRONIC_URL}/${SUPERCRONIC}"
echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c -
chmod +x "${SUPERCRONIC}"
mv "${SUPERCRONIC}" "/usr/local/bin/${SUPERCRONIC}"
ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic
EOT
# Entrypoint file 'entrypoint.sh' is common for Webserver, Cron and Horizon Docker images
COPY --chown=www-data:www-data --chmod=744 entrypoint.sh /entrypoint.sh
# CMD for 'cron' image is 'cmd-cron.sh'
COPY --chown=www-data:www-data --chmod=744 cmd-cron.sh /cmd.sh
# Switch to application user provided by 'php:8.3-fpm' image
USER www-data
# Create directory with 'www-data' ownership
WORKDIR /app
# Run Laravel Schedule on every minute
COPY --chown=www-data:www-data --chmod=644 <<EOT crontab
*/1 * * * * cd /app && php artisan schedule:run --no-ansi
EOT
ENTRYPOINT ["/entrypoint.sh"]
CMD ["/cmd.sh"]
I have intentionally skipped some stuff related to PHP extensions and Composer just to focus on cron-related tasks.
Currently docker build
will obviously not work because of missing entrypoint.sh
and cmd-cron.sh
.
Minimal entrypoint.sh
can be something like this:
#!/usr/bin/env bash
set -e
echo "Checking app configuration"
# Your entrypoint tasks
echo "Clearing app cache"
echo "php artisan config:cache --no-ansi"
# Other tasks
exec /usr/local/bin/docker-php-entrypoint "$@"
Finally cron-cmd.sh
, a little hacky, but working like a charm:
#!/usr/bin/env bash
set -e
supercronic_pid=0
sig_handler() {
if [ $supercronic_pid -ne 0 ]; then
# Notify Laravel Schedule to not take any more commands to run
echo "php /app/artisan schedule:interrupt --no-ansi"
# Notify Supercronic it should stop working
kill -SIGTERM "$supercronic_pid"
wait "$supercronic_pid"
fi
# Exit 143 in Docker world means container terminated gracefully
exit 143
}
# Setup signal trap
trap 'sig_handler' SIGTERM SIGQUIT SIGINT
# Remove any open Laravel Schedule locks on startup
echo "php /app/artisan schedule:clear-cache --no-ansi"
# Spawn Supercronic process in the background
supercronic crontab &
# And catch its PID into variable
supercronic_pid="$!"
# Supercronic should never exit on its own
wait "$supercronic_pid"
# If it does, it means an abnormal exit occurred
exit 1
You can notice two Laravel artisan commands here:
-
schedule:clear-cache
to remove any locks created bywithoutOverlapping()
method -
schedule:interrupt
to notify Laravel scheduler about incoming container termination
You cannot simply exec supercronic crontab
in cron-cmd.sh
, because you will stop receiving termination signals. exec
in bash will replace current process PID, and would change to supercronic
in this case.
Top comments (2)