How to run Laravel scheduler in Docker container
Tags: laravel, docker, cron, bash
Published: 2024-03-10 21:24:39

---
title: How to run Laravel scheduler in Docker container
published: true
description: 
tags: laravel, docker, cron, bash
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-02-12 19:19 +0000
---
There is nothing easier than adding
`* * * * * cd /your-project && php artisan schedule:run`
to crontab according to [Laravel Docs](https://laravel.com/docs/10.x/scheduling#running-the-scheduler), 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 by `withoutOverlapping()` [method](https://laravel.com/docs/10.x/scheduling#preventing-task-overlaps)
- `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.