supervisorもcronも使わずにLaravelのScheduleとQueueを実行する【Docker】
はじめに
laravelで動かしているプロジェクトをコンテナ化するときに、supervisorとcronで動かしているスケジューラやキューワーカをどうしようか迷っていました。
一応、1コンテナで複数プロセスを実行する方法としては、Laravel Sailではsupervisorを使用していたり[1]、公式ドキュメントにはsupervisorの使い方が書いていたり[2]するものの、公式ドキュメントのベストプラクティスには以下の記載があります。
Each container should have only one concern. Decoupling applications into multiple containers makes it easier to scale horizontally and reuse containers.
-- 各コンテナはただ1つだけの用途を持つべきです。アプリケーションを複数のコンテナに切り離すことで、水平スケールやコンテナの再利用がより簡単になります。
https://docs.docker.com/build/building/best-practices/#decouple-applications
また、ビルト時にsupervisorやcronをインストールしてイメージサイズが大きくなるのも嫌だったので、タスクスケジューラとキューワーカをアプリケーションとは別のコンテナに分け、それぞれ独立したコンテナで実行させるようにしました。
実行環境
- Ubuntu 24.04 LTS
- Docker version 27.1.1
- Docker Compose version v2.29.1
- Laravel 11.x
最終的な成果物
とりあえずコピペで動かしたい人向け
/home/ubuntu/my-app
├ compose.yaml
├ docker
| ├ etc
| | └ start.sh
| └ php
| └ Dockerfile
└ src ............. Laravelのなかみ
├ app
FROM dunglas/frankenphp
WORKDIR /app
RUN install-php-extensions \
pcntl \
intl \
redis \
pdo_mysql \
bcmath \
zip \
opcache \
@composer
COPY ./docker/etc/start.sh /usr/local/bin/start
RUN chmod u+x /usr/local/bin/start
CMD ["/usr/local/bin/start"]
#!/usr/bin/env bash
set -e
role=${CONTAINER_ROLE:-app}
if [ "$role" = "app" ]; then
frankenphp php-server
elif [ "$role" = "scheduler" ]; then
while [ true ]
do
php artisan schedule:run --verbose --no-interaction &
sleep 60
done
elif [ "$role" = "queue" ]; then
echo "Running the queue..."
php artisan queue:work --verbose --tries=3 --timeout=90
else
echo "Could not match the container role \"$role\""
exit 1
fi
services:
app:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: app
depends_on:
- redis
- mysql
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
- $PWD/src:/app
mysql:
image: mysql:8.4
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_DATABASE: myapp
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: myapp_user
MYSQL_PASSWORD: secret
redis:
image: redis:7.4-alpine
command: redis-server --appendonly yes --requirepass secret
ports:
- "6379:6379"
volumes:
- redis_data:/data
scheduler:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: scheduler
depends_on:
- app
volumes:
- $PWD/src:/app
queue:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: queue
depends_on:
- app
volumes:
- $PWD/src:/app
Laravel+Mysql+Redis
はじめに、プロジェクトのベースとなるコンテナ群を作成します。
Docker環境を作るためのファイル群を用意しておきます。
touch compose.yml
mkdir -p docker/php
touch docker/php/Dockerfile
mkdir -p docker/etc
touch docker/etc/start.sh
mkdir -p src
compose.yaml
services:
app:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: app
depends_on:
- redis
- mysql
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
- $PWD/src:/app
mysql:
image: mysql:8.4
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_DATABASE: myapp
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: myapp_user
MYSQL_PASSWORD: secret
redis:
image: redis:7.4-alpine
command: redis-server --appendonly yes --requirepass secret
ports:
- "6379:6379"
volumes:
- redis_data:/data
appコンテナで環境変数にCONTAINER_ROLE
をセットしています。
後述のキューワーカーやスケジューラのコンテナはこの値を変更することで、同じイメージを使用して別のコマンドを実行することになります。
また、ホストのsrcディレクトリは未だ空ですが、のちにlaravelプロジェクトのソースが入ります。
ホストのsrcディレクトリとコンテナのappディレクトリをマウントすることで、ソースの変更を反映させます。
Dockerfile
FROM dunglas/frankenphp
WORKDIR /app
RUN install-php-extensions \
pcntl \
intl \
redis \
pdo_mysql \
bcmath \
zip \
opcache \
@composer
COPY ./docker/etc/start.sh /usr/local/bin/start
RUN chmod u+x /usr/local/bin/start
CMD ["/usr/local/bin/start"]
今回はfrankenphpを使用しています。
frankenphpはPHPを実行可能なWebサーバーかつ、PHPを実行可能なCLIを提供しています。よくあるphp-fpm+nginx(orApache)構成がfrankenphp一つでできて、しかも速い[3]です。
また、ホスト上のbashスクリプトをコンテナ上にコピーし、実行権限を追加、最後にコピーしたbashスクリプトをコンテナ上で実行しています。
start.sh
#!/usr/bin/env bash
set -e
role=${CONTAINER_ROLE:-app}
if [ "$role" = "app" ]; then
echo "Running php-server..."
frankenphp php-server
else
echo "Could not match the container role \"$role\""
exit 1
fi
bashスクリプトでは、web+phpサーバーを起動するコマンド(frankenphp php-server
)を実行しています。
また、bashでは変数のデフォルト値を設定できるので、環境変数CONTAINER_ROLE
が設定されていない場合はapp ロールをデフォルトにします(role=${CONTAINER_ROLE:-app}
)。この値は前述のcompose.yml
で設定しています。
今回は検証用に1分置きにHello!
と出力するscheduleをあらかじめ登録します。
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command(fn() {
echo 'Hello!';
})->everyMinute();
}
docker compose up --build
この状態で一度ビルドし、新しいlaravelプロジェクトを作ります。
docker compose up --build
docker compose exec app composer create-project --prefer-dist laravel/laravel .
docker compose exec app php artisan key:generate
docker compose exec app php artisan storage:link
docker compose exec app chmod -R 777 storage bootstrap/cache
docker compose exec app php artisan migrate
https://localhost
にアクセスすると、Laravel のウェルカム ページを取得できるはずです。
Laravel scheduler
次に、Laravelのスケジューラを実行するコンテナを作ります。
Laravelの公式ドキュメントでは、cronを使用して1 分ごとにコマンドを実行するcrontabを登録していました。[4]
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1
が、今回はcronを使用しないので、bashを使用してこれと同じ処理を実行させます。
start.sh
#!/usr/bin/env bash
set -e
role=${CONTAINER_ROLE:-app}
if [ "$role" = "app" ]; then
frankenphp php-server
elif [ "$role" = "scheduler" ]; then
while [ true ]
do
php artisan schedule:run --verbose --no-interaction &
sleep 60
done
else
echo "Could not match the container role \"$role\""
exit 1
fi
無限ループするwhileの中に、laravelのスケジューラを実行するコマンドを登録します。
また、sleep 60
とすることで1分おきの実行を実現しています。
compose.yaml
services:
app:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: app
depends_on:
- redis
- mysql
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
- $PWD/src:/app
mysql:
image: mysql:8.4
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_DATABASE: myapp
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: myapp_user
MYSQL_PASSWORD: secret
redis:
image: redis:7.4-alpine
command: redis-server --appendonly yes --requirepass secret
ports:
- "6379:6379"
volumes:
- redis_data:/data
scheduler:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: scheduler
depends_on:
- app
volumes:
- $PWD/src:/app
laravelのスケジューラを実行するコンテナを追加します。
CONTAINER_ROLE
にschedulerを指定することで、前述したstart.shのschedule:runを実行するようになります。
また、appコンテナと同じDockerfileを指定することで重複するDockerイメージをビルドしなくて済みます。
docker compose up --build
start.shの更新を反映させるため、もう一度ビルドし、コンテナを起動します。
docker compose up --build -d
この状態でコンテナのログを起動すると、スケジューラが起動していることがわかります。
docker compose logs scheduler
my-app-scheduler-1 | No scheduled commands are ready to run.
my-app-scheduler-1 | Running scheduled command: Closure Hello!
Laravel queue worker
最後に、キューワーカコンテナを作ります。
compose.yaml
services:
app:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: app
depends_on:
- redis
- mysql
ports:
- "80:80"
- "443:443"
volumes:
- caddy_data:/data
- caddy_config:/config
- $PWD/src:/app
mysql:
image: mysql:8.4
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
environment:
MYSQL_DATABASE: myapp
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: myapp_user
MYSQL_PASSWORD: secret
redis:
image: redis:7.4-alpine
command: redis-server --appendonly yes --requirepass secret
ports:
- "6379:6379"
volumes:
- redis_data:/data
scheduler:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: scheduler
depends_on:
- app
volumes:
- $PWD/src:/app
queue:
build:
context: .
dockerfile: ./docker/php/Dockerfile
environment:
CONTAINER_ROLE: queue
depends_on:
- app
volumes:
- $PWD/src:/app
queueコンテナを追加し、CONTAINER_ROLEにqueueを設定しています。
start.sh
#!/usr/bin/env bash
set -e
role=${CONTAINER_ROLE:-app}
if [ "$role" = "app" ]; then
frankenphp php-server
elif [ "$role" = "scheduler" ]; then
while [ true ]
do
php artisan schedule:run --verbose --no-interaction &
sleep 60
done
elif [ "$role" = "queue" ]; then
echo "Running the queue..."
php artisan queue:work --verbose --tries=3 --timeout=90
else
echo "Could not match the container role \"$role\""
exit 1
fi
CONTAINER_ROLEにqueueであるコンテナの実行コマンドとしてphp artisan queue:work --verbose --tries=3 --timeout=90
を指定しています。
docker compose up --build
start.shの更新を反映させるため、もう一度ビルドし、コンテナを起動します。
docker compose up --build -d
この状態でコンテナのログを起動すると、キューワーカーが起動していることがわかります。
docker compose logs queue
my-app-queue-1 | Running the queue...
おまけ
healthcheck
スケジューラとキューワーカコンテナが正常に作動しているかを確認するために、Dockerのhealthcheckを利用して1分ごとにキューの監視を行います。
services:
...
scheduler:
...
healthcheck:
test: ["CMD", "php", "artisan", "queue:monitor", "redis:default", "--max=100"]
interval: 60s
timeout: 5s
worker:
...
healthcheck:
test: ["CMD", "php", "artisan", "queue:monitor", "redis:default", "--max=100"]
interval: 60s
timeout: 5s
...
max=100を超えるとLaravelがQueueイベントを発行するので、このイベントが発行されたら公式ドキュメントのようにslackなどに通知するようにしてもいいと思います。[5]
参考
-
https://github.com/laravel/sail/blob/1.x/runtimes/8.3/start-container#L25 ↩︎
-
https://docs.docker.com/engine/containers/multi-service_container/#use-a-process-manager ↩︎
-
https://dev.to/dimdev/performance-benchmark-of-php-runtimes-2lmc ↩︎
-
https://laravel.com/docs/11.x/scheduling#running-the-scheduler ↩︎
-
https://laravel.com/docs/11.x/queues#monitoring-your-queues ↩︎
Discussion