在 Docker 中运行 Laravel Scheduler 和 Queue

发布于 作者

Running the Laravel Scheduler and Queue with Docker image

在 Laravel 中,从虚拟服务器切换到 Docker 时,一个棘手的变化是弄清楚如何运行调度程序和队列工作者。当 PHP 开发人员尝试弄清楚如何在 Docker 中使用 Laravel 时,我经常看到这个问题。

您应该在主机服务器上运行它们吗?您应该通过 Docker 容器中的 cron 运行它们吗?

我推荐在 Docker 中运行调度程序命令和 Laravel 队列的几种方法,我们将介绍使用完整的(尽管很简单)Docker 设置运行两者的基础知识,您可以使用它来进行实验。

多用途 Docker 镜像

在 Docker 的背景下,您可以将工作负载分成单独的容器并独立扩展它们。您可以有多个容器运行队列工作者,一个容器运行调度程序,以及多个容器运行您的 Web 应用程序。Laravel 的设计是单体的,这意味着您的队列作业、计划命令和 HTTP 端点共享一个代码库。

当您开始使用 Docker 将 HTTP 流量、队列和计划命令分隔开来时,您必须对如何为每个用途构建镜像做出一些决定。

例如,您是否为每个运行 Laravel 代码的上下文定义一个单独的Dockerfile?一个用于您的 Web 应用程序,一个用于您的队列,一个用于您的调度程序?

我建议,通过一些巧妙的脚本,我们可以构建一个单个灵活的 Docker 镜像,它可以支持所有三种角色。这意味着构建一个镜像,它可以作为 Web 服务器、调度程序运行器或队列工作者运行。

使用 Docker,您可以以传统服务器上无法实现的新颖且令人兴奋的方式分割您的工作负载。虽然在 Ubuntu 服务器上,您可能会在同一台机器上运行您的 Web 服务器、队列和调度程序命令。但是,使用 Docker,在同一个容器中运行所有这些进程没有意义。

让我们看看如何使用 bash 脚本运行我们的 Docker CMD 来实现这一点。

项目设置

在我们深入研究如何在 Docker 中以不同的角色运行应用程序之前,让我们使用 Apache Web 服务器设置一个简单的 Laravel 项目和 Docker。

我们将使用官方 php Docker 镜像作为我们的基本镜像,以及 Docker Compose 来运行 MySQL 和 Redis。首先,让我们设置我们需要的文件来设置 Docker 环境

laravel new docker-laravel
cd ./docker-laravel
mkdir docker/
touch docker-compose.yml
touch docker/Dockerfile
touch docker/start.sh
touch docker/vhost.conf

Docker Compose

这篇文章不是关于使用 Docker Compose 的,所以如果你不熟悉它,我建议你阅读Docker Compose 文档,以及我的Docker PHP 书(还有一个单独的书版本)在整本书中介绍了很多 Docker Compose 示例。

以下是包含运行应用程序服务所需的服务的 Docker Compose 文件

version: "3"
services:
app:
image: laravel-www
container_name: laravel-www
build:
context: .
dockerfile: docker/Dockerfile
depends_on:
- redis
- mysql
ports:
- 8080:80
volumes:
- .:/var/www/html
environment:
APP_ENV: local
CONTAINER_ROLE: app
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_DRIVER: redis
REDIS_HOST: redis
 
redis:
container_name: laravel-redis
image: redis:4-alpine
ports:
- 16379:6379
volumes:
- redis:/data
 
mysql:
container_name: laravel-mysql
image: mysql:5.7
ports:
- 13306:3306
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: homestead
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: homestead
MYSQL_PASSWORD: secret
 
volumes:
redis:
driver: "local"
mysql:
driver: "local"

请注意CONTAINER_ROLE环境变量,当我们开始完善自定义 Docker 脚本时,它将发挥作用。我们还为 MySQL 定义了一些开发数据库设置,并设置了卷来持久保存我们的 MySQL 和 Redis 数据。

app服务中,我们挂载了一个卷,以便我们的 PHP 和前端代码更改在开发过程中立即反映出来。

Dockerfile

我们设置了 MySQL、Redis 和 Laravel 应用程序服务,它们将从docker/Dockerfile构建,我们需要更新它

FROM php:7.2-apache-stretch
 
COPY . /var/www/html
COPY docker/vhost.conf /etc/apache2/sites-available/000-default.conf
 
RUN chown -R www-data:www-data /var/www/html \
&& a2enmod rewrite

Dockerfile从官方 PHP Apache 镜像获取大部分功能。我们正在复制 Laravel 代码和一个 Apache Vhost 文件。Vhost 文件覆盖是必需的,以便我们可以将DocumentRoot指向/var/www/html/public,这是 Laravel 所期望的。

然后,我们将文件的所属权更改为www-data用户,以便在生产环境中拥有正确的权限。最后,我们启用 mod_rewrite,以便 URL 重写可以正常工作。

Apache Vhost

接下来,我们需要填写docker/vhost.conf文件以指向我们的public文件夹。将以下内容添加到docker/vhost.conf中,该文件被复制到镜像中,覆盖了000-default.confvhost 文件

<VirtualHost *:80>
DocumentRoot /var/www/html/public
 
<Directory "/var/www/html/public">
AllowOverride all
Require all granted
</Directory>
 
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>

此时,您可以构建镜像,并且您应该能够在localhost:8080上获得 Laravel 的欢迎页面

docker-compose up --build

自定义 Docker CMD 指令

我们的docker/Dockerfile使用 Debian Stretch 扩展了官方 PHP Apache 镜像。如果你仔细查看我们扩展的基本 Apache 镜像,你会看到一个CMD指令

CMD ["apache2-foreground"]

我们将在自己的 Dockerfile 中覆盖 CMD 指令,所以让我们从构建 bash 脚本的结构开始,并在脚本中使用apache2-foreground。然后,我们将扩展脚本以包括调度程序和队列的角色。

更新docker/Dockerfile使其看起来像下面这样

FROM php:7.2-apache-stretch
 
COPY . /var/www/html
COPY docker/vhost.conf /etc/apache2/sites-available/000-default.conf
COPY docker/start.sh /usr/local/bin/start
 
RUN chown -R www-data:www-data /var/www/html \
&& chmod u+x /usr/local/bin/start \
&& a2enmod rewrite
 
CMD ["/usr/local/bin/start"]

我们现在从docker/start.sh中复制一个 bash 脚本到/usr/local/bin/start,并使其可执行。最后,我们覆盖 CMD 指令以运行我们的 bash 脚本。

在我们填写队列工作者之前,让我们让 apache 服务器再次工作。将以下内容添加到docker/start.sh

#!/usr/bin/env bash
 
set -e
 
role=${CONTAINER_ROLE:-app}
env=${APP_ENV:-production}
 
if [ "$env" != "local" ]; then
echo "Caching configuration..."
(cd /var/www/html && php artisan config:cache && php artisan route:cache && php artisan view:cache)
fi
 
if [ "$role" = "app" ]; then
 
exec apache2-foreground
 
elif [ "$role" = "queue" ]; then
 
echo "Queue role"
exit 1
 
elif [ "$role" = "scheduler" ]; then
 
echo "Scheduler role"
exit 1
 
else
echo "Could not match the container role \"$role\""
exit 1
fi

这个脚本中有很多内容。首先,我们正在检查除local以外的环境,并运行类似生产的缓存。我们能够缓存配置和路由的事实值得使用自定义 bash 脚本,即使您不打算运行队列或调度程序。请注意,如果您不小心在没有APP_ENV=local环境的情况下运行容器,则需要清除配置缓存php artisan config:clear,以避免在开发过程中出现缓存的值。

在 bash 中,您可以为变量设置默认值,因此如果CONTAINER_ROLE环境变量未设置,我们将使app成为默认角色。请注意,我们正在docker-compose.yml文件中设置它。

最后,我们脚本的核心是基于容器角色的逻辑。现在,我们只是echo调度程序和队列的角色类型,并在app角色中运行exec apache2-foreground来启动 Apache。

如果您重建 Docker 镜像并再次运行 Docker compose,启动脚本应该运行 Apache 并提供 Laravel 应用程序

docker-compose down
docker-compose build
docker-compose up

运行调度程序

我们可以修改我们的docker/start.sh脚本以运行调度程序。这个方法的妙处在于,我们可以使用单个 Docker 镜像来实现这一点,并根据角色在运行时修改它的工作方式。

如果您是在传统服务器上设置调度程序,您将为它设置一个 Cron 条目,如下所示

* * * * * php /path-to-your-project/artisan schedule:run >> /dev/null 2>&1

Cron 条目被定义为使脚本每 60 秒运行一次。我们可以使用 bash 在没有在 Docker 镜像中安装 Cron 的情况下模拟这种行为。棘手的地方在于我们的容器必须保持一个进程在前台运行,否则容器将退出。

我们可以使用无限的 bash 循环来实现与 cron 相同的设计。

if [ "$role" = "app" ]; then
 
exec apache2-foreground
 
elif [ "$role" = "queue" ]; then
 
echo "Queue role"
exit 1
 
elif [ "$role" = "scheduler" ]; then
 
while [ true ]
do
php /var/www/html/artisan schedule:run --verbose --no-interaction &
sleep 60
done
 
else
echo "Could not match the container role \"$role\""
exit 1
fi

如果我们从镜像中运行一个带有 CONTAINER_ROLE=scheduler 的容器,一个无限的 bash while 循环将在前台运行,并且每 60 秒一个新的 schedule:run 命令将在后台运行(在末尾使用 &)。

在后台运行此命令对于让它像 Cron 一样工作至关重要。如果我们在前台运行 schedule:run 命令,脚本将停止执行,并且 sleep 60 不会在 schedule:run 命令完成之前执行。

为了尝试一下,请在 app/Console/Kernel.php 文件中取消 inspire 命令的注释(或添加您自己的命令)。

/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('inspire')
->everyMinute();
}

接下来,我们需要在 docker-compose.yml 文件中定义一个依赖于 www-laravel 镜像的调度程序服务。

scheduler:
image: laravel-www
container_name: laravel-scheduler
depends_on:
- app
volumes:
- .:/var/www/html
environment:
APP_ENV: local
CONTAINER_ROLE: scheduler
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_DRIVER: redis
REDIS_HOST: redis

请注意,我们在 appscheduler 服务之间复制了一些环境变量的值。您可以使用专用的 Docker compose env_file 属性 或使用 Laravel 的 .env 文件来优化它。我更喜欢在开发中使用外部 Docker 环境文件。

以下列出了完整的 docker-compose.yml 文件供参考。我们现在还将添加 queue 服务,这样您以后就不必再添加了。

version: "3"
services:
app:
image: laravel-www
container_name: laravel-www
build:
context: .
dockerfile: docker/Dockerfile
depends_on:
- redis
- mysql
ports:
- 8080:80
volumes:
- .:/var/www/html
environment:
APP_ENV: local
CONTAINER_ROLE: app
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_DRIVER: redis
REDIS_HOST: redis
 
scheduler:
image: laravel-www
container_name: laravel-scheduler
depends_on:
- app
volumes:
- .:/var/www/html
environment:
APP_ENV: local
CONTAINER_ROLE: scheduler
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_DRIVER: redis
REDIS_HOST: redis
 
queue:
image: laravel-www
container_name: laravel-queue
depends_on:
- app
volumes:
- .:/var/www/html
environment:
APP_ENV: local
CONTAINER_ROLE: queue
CACHE_DRIVER: redis
SESSION_DRIVER: redis
QUEUE_DRIVER: redis
REDIS_HOST: redis
 
redis:
container_name: laravel-redis
image: redis:4-alpine
ports:
- 16379:6379
volumes:
- redis:/data
 
mysql:
container_name: laravel-mysql
image: mysql:5.7
ports:
- 13306:3306
volumes:
- mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: homestead
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: homestead
MYSQL_PASSWORD: secret
 
volumes:
redis:
driver: "local"
mysql:
driver: "local"

我们还需要构建镜像,以便对 docker/start.sh 文件的更新成为镜像构建的一部分。

docker-compose down
docker-compose build
docker-compose up

如果一切正常,您应该看到调度程序容器的以下输出。

laravel-scheduler | No scheduled commands are ready to run.
laravel-scheduler | Running scheduled command: '/usr/local/bin/php' 'artisan' inspire > '/dev/null' 2>&1
laravel-scheduler | Running scheduled command: '/usr/local/bin/php' 'artisan' inspire > '/dev/null' 2>&1
laravel-scheduler | Running scheduled command: '/usr/local/bin/php' 'artisan' inspire > '/dev/null' 2>&1

关于在 Docker 中运行调度程序,还需要注意的一点是:从 Laravel 5.6 开始,您可以运行 onOneServer() 命令,该命令表示命令仅在一个服务器上运行。您需要使用 Memcached 或 Redis 缓存驱动程序来支持此功能。

队列工作者

我们将要更新的 Image 的最后一件事是支持运行队列工作者。我们将使用 Redis,因此需要 predis/predis Composer 依赖项。在本地运行此命令以安装 predis。

composer require predis/predis

接下来,更新 docker/start.sh 脚本,如果 $role=queue 则运行队列工作者。

if [ "$role" = "app" ]; then
 
exec apache2-foreground
 
elif [ "$role" = "queue" ]; then
 
echo "Running the queue..."
php /var/www/html/artisan queue:work --verbose --tries=3 --timeout=90
 
elif [ "$role" = "scheduler" ]; then
# ...

以下是完整的最终启动脚本文件。

#!/usr/bin/env bash
 
set -e
 
role=${CONTAINER_ROLE:-app}
env=${APP_ENV:-production}
 
if [ "$env" != "local" ]; then
echo "Caching configuration..."
(cd /var/www/html && php artisan config:cache && php artisan route:cache && php artisan view:cache)
fi
 
if [ "$role" = "app" ]; then
 
exec apache2-foreground
 
elif [ "$role" = "queue" ]; then
 
echo "Running the queue..."
php /var/www/html/artisan queue:work --verbose --tries=3 --timeout=90
 
elif [ "$role" = "scheduler" ]; then
 
while [ true ]
do
php /var/www/html/artisan schedule:run --verbose --no-interaction &
sleep 60
done
 
else
echo "Could not match the container role \"$role\""
exit 1
fi

如果您创建了一个测试队列作业并将其调度(例如,您可以从欢迎路由内调度它),您将在队列处理期间在 docker-compose 日志中看到类似于以下的输出。

laravel-queue | [2018-04-25 06:45:59][qv5tfCgvUsKxVTTvq0SsF6gx9VLwnd6H] Processing: App\Jobs\ExampleJob
laravel-queue | [2018-04-25 06:45:59][qv5tfCgvUsKxVTTvq0SsF6gx9VLwnd6H] Processed: App\Jobs\ExampleJob

了解更多

如果您想了解更多关于使用 Docker 和 PHP(包括 Laravel)进行开发的信息,请查看我的书 Docker for PHP Developers。如果您不想要 Laravel 初学者代码,您也可以获得 仅书籍版本。Laravel News 的读者可以在这两个版本的书籍上享受折扣!您可以 在这里了解更多关于这本书的信息

要了解更多关于运行自定义启动脚本的信息,我建议您阅读更多关于来自 Docker 官方参考的 CMD Dockerfile 指令 的信息,了解它的工作原理。

The offical Docker PHP image 有很多优秀的文档,我建议您通读这些文档,了解如何快速将 PHP 镜像用于您的 Docker 项目。我们使用了 Apache,但您可以在各种不同的镜像类型(例如 Alpine、Debian Stretch、Debian Jesse)中使用 PHP-FPM。


包含的链接是联盟链接,这意味着如果您决定购买,Laravel News 会获得一小部分回扣来帮助运营这个网站。

Paul Redmond photo

Laravel News 的撰稿人。全栈 Web 开发人员和作家。

Cube

Laravel 新闻

加入 40,000 多名其他开发人员,永不错过新的技巧、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理您的服务器,并在几秒钟内部署您的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和对本地和生产环境的即时反馈来进行调试。

Tinkerwell
No Compromises logo

没有妥协

来自 No Compromises 播客的两位经验丰富的开发人员 Joel 和 Aaron 现在可供您为您的 Laravel 项目聘用。 ⬧ 固定价格 7500 美元/月。 ⬧ 没有冗长的销售流程。 ⬧ 没有合同。 ⬧ 100% 退款保证。

没有妥协
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保您的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动的 Laravel 升级和代码现代化,使您的应用程序保持新鲜。

Shift
Bacancy logo

Bacancy

只需 2500 美元/月,即可为您的项目配备经验丰富的 Laravel 开发人员,拥有 4-6 年的经验。获得 160 小时的专业知识和 15 天无风险试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

立即获得幸运 - 拥有十年以上经验,是 Laravel 开发的理想选择!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源包,将现代无头电子商务功能的强大功能带到 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 职位板

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,它具有运行现代 SaaS 所需的所有功能。支付、漂亮的结账、管理面板、用户仪表板、身份验证、现成的组件、统计数据、博客、文档等等。

SaaSykit: Laravel SaaS 启动工具包
Rector logo

Rector

您无缝 Laravel 升级的合作伙伴,降低成本,加快创新,为成功的公司提供助力。

Rector
MongoDB logo

MongoDB

通过 MongoDB 和 Laravel 的强大集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。支持事务性、搜索、分析和移动用例,同时使用熟悉的 Eloquent API。了解 MongoDB 的灵活、现代数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的无依赖项输入掩码库。

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel 提示构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包将评论添加到您的 Laravel 应用程序

阅读文章