在 Docker 中运行 Laravel Scheduler 和 Queue
发布于 作者 Paul Redmond
在 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-laravelcd ./docker-laravelmkdir docker/touch docker-compose.ymltouch docker/Dockerfiletouch docker/start.shtouch 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/htmlCOPY 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.conf
vhost 文件
<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/htmlCOPY docker/vhost.conf /etc/apache2/sites-available/000-default.confCOPY 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 1fi
这个脚本中有很多内容。首先,我们正在检查除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 downdocker-compose builddocker-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 1fi
如果我们从镜像中运行一个带有 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
请注意,我们在 app
和 scheduler
服务之间复制了一些环境变量的值。您可以使用专用的 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 downdocker-compose builddocker-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>&1laravel-scheduler | Running scheduled command: '/usr/local/bin/php' 'artisan' inspire > '/dev/null' 2>&1laravel-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 1fi
如果您创建了一个测试队列作业并将其调度(例如,您可以从欢迎路由内调度它),您将在队列处理期间在 docker-compose 日志中看到类似于以下的输出。
laravel-queue | [2018-04-25 06:45:59][qv5tfCgvUsKxVTTvq0SsF6gx9VLwnd6H] Processing: App\Jobs\ExampleJoblaravel-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 会获得一小部分回扣来帮助运营这个网站。