很少有技术能像容器那样获得广泛的应用。今天,每一种主要的编程语言、操作系统、工具、云供应商、持续集成(CI)和持续部署或交付(CD)平台都有对容器的本地支持。
容器的普及在很大程度上是由开发者和运营团队采用的,他们已经看到了第一手的好处。
在这篇文章中,我们将学习如何将一个现有的Laravel应用程序容器化,并在此过程中,我们将探索容器可以为开发人员的工作流程带来的一些好处。
示例应用程序
在这篇博文中, 我们将对GitHub上提供的Laravel任务列表应用样本进行容器化。这个项目是针对一个相当老的Laravel版本编写的, 并且已经被归档.然而, 使用这个代码库需要我们解决一些在生产和遗留代码库中常见的需求, 例如使用特定版本的PHP, 数据库迁移, 端口映射, 和运行集成测试.
示例应用程序的最终版本看起来像这样:

先决条件
要跟上这篇博文,你必须在你的本地机器上安装Docker。
Windows和MacOS用户可以在Docker网站上找到下载和安装说明。
Docker还为许多Linux发行版提供了软件包。
请注意,Windows最近开始为容器提供本地支持。这使得本地Windows软件可以在容器内运行。然而,这篇文章假设Windows开发人员正在运行Docker Desktop与Linux容器。下面的PowerShell命令确保Docker for Windows被设置为与Linux容器一起工作。
& "C:\Program Files\Docker\Docker\DockerCli.exe" -SwitchLinuxEngine
由于应用程序的源代码存储在GitHub上,因此还必须安装git 命令行(CLI)工具。git文档提供了安装说明。
要在容器外运行应用程序,必须安装PHP 7。PHP网站提供了PHP 7的下载。
我们还需要Composer来安装依赖项。Composer文档提供了关于下载和安装该工具的说明。
注意由于composer.json 文件中定义了特定的版本要求,该应用程序不会在PHP 8上运行。如果你试图用composer install --ignore-platform-reqs 命令绕过这个要求,会显示以下错误。
Uncaught ErrorException: Method ReflectionParameter::getClass() is deprecated
此方法的废弃在此有记录。虽然如果你愿意更新源代码,有一些选项可以替代这些被废弃的方法,但我们会把它当作一个利用容器来解决特定依赖关系的机会。
克隆应用程序的源代码
要克隆git仓库到你的本地工作站,运行以下命令。
git clone https://github.com/laravel/quickstart-basic quickstart
这将创建一个名为quickstart 的目录,其中包含应用程序的源代码。
在容器外运行应用程序
要在容器外运行应用程序,我们首先需要安装依赖项。
composer install
接下来,数据库必须被初始化。该样本应用程序支持许多数据库,包括流行的开源项目,如MySQL和PostgreSQL。默认设置为MySQL,所以我们将在这篇文章中使用它。
注意,我们没有在先决条件部分列出MySQL。这是因为我们将在一个容器中运行MySQL,这允许我们用一个docker 命令来下载、配置和运行MySQL。
下面的Bash命令将MySQL作为一个Docker容器运行。
docker run --name mysql \
-d \
-p 3306:3306 \
-e MYSQL_RANDOM_ROOT_PASSWORD=true \
-e MYSQL_DATABASE=laravel \
-e MYSQL_USER=laravel \
-e MYSQL_PASSWORD=Password01! \
mysql
这里是PowerShell的相同命令。
docker run --name mysql `
-d `
-p 3306:3306 `
-e MYSQL_RANDOM_ROOT_PASSWORD=true `
-e MYSQL_DATABASE=laravel `
-e MYSQL_USER=laravel `
-e MYSQL_PASSWORD=Password01! `
mysql
这个命令做了很多工作,所以让我们把它分解一下。
run 参数指示Docker运行作为最后参数传递的Docker镜像,也就是mysql 。正如我们在介绍中指出的,每个主要工具都提供了一个支持的Docker镜像,MySQL也不例外。MySQL镜像的来源是默认的Docker注册表,称为DockerHub,它提供了存储库mysql。
我们用参数--name mysql 来定义Docker容器的名称。如果我们不指定名字,Docker会随机分配一个(通常是幽默的)名字,比如modest_shaw 或nervous_goldwasser 。
-d 参数在后台运行该容器。
容器要暴露的端口是用-p 参数定义的。在这里,我们将本地端口3306 暴露给容器端口3306 。
环境变量是用-e 参数定义的。我们设置了MYSQL_RANDOM_ROOT_PASSWORD,MYSQL_DATABASE,MYSQL_USER, 和MYSQL_PASSWORD 环境变量,以配置MySQL的随机根密码,一个名为laravel 的初始数据库,一个名为laravel 的用户账户,以及Password01! 的用户密码。
最后一个参数mysql 是用于创建容器的Docker镜像的名称。
下载和运行工具的能力,正如我们对MySQL所做的那样,是Docker和容器的主要好处之一。在先决条件部分提到的安装特定版本的PHP和Composer需要访问多个网页来下载特定的软件包或运行独特的安装命令,而运行Docker镜像的命令对每个操作系统和每个Docker镜像的标签都是一样的。
很多Docker特有的术语,如注册表、存储库、镜像和容器,被用来描述上述命令。我们将在后面的章节中介绍这些术语,但现在,我们将继续在本地运行我们的示例应用程序。
为了初始化数据库,我们首先设置了一些环境变量,这些变量由应用程序读取以连接到数据库。你会注意到,这些值与作为容器运行MySQL时使用的值一致。
下面的命令在Bash中设置环境变量。
export DB_HOST=localhost
export DB_DATABASE=laravel
export DB_USERNAME=laravel
export DB_PASSWORD=Password01!
这些是相当于PowerShell的命令。
$env:DB_HOST="localhost"
$env:DB_DATABASE="laravel"
$env:DB_USERNAME="laravel"
$env:DB_PASSWORD="Password01!"
然后我们用下面的命令运行数据库迁移。
php artisan migrate
用下面的命令来运行测试。
vendor/bin/phpunit
最后,我们用下面的命令运行由开发网络服务器托管的样本应用程序。
php artisan serve
然后,我们可以打开http://localhost:8000,查看任务列表。
尽管我们使用Docker来下载和运行MySQL,只用了一个命令,但需要安装很多工具。其中一些工具需要特定的版本,通常操作系统之间的进程略有不同,必须执行许多单独的命令才能达到运行这个简单的应用程序的目的。
Docker可以在这里帮助我们,让我们只需调用少量的docker ,就可以编译、测试和运行我们的代码。正如我们所看到的,我们也可以通过docker 命令来运行其他应用程序,如MySQL。最重要的是,这些命令对每个操作系统都是一样的。
不过,在我们继续之前,让我们更详细地挖掘一下上面使用的一些Docker术语。
Docker注册表和存储库
Docker注册表很像Linux软件包管理器(如apt 或yum )、Windows软件包管理器(如Chocolatey)或MacOS软件包管理器(如Brew)使用的在线软件包集合。Docker注册中心是一个Docker仓库的集合,每个Docker仓库提供Docker镜像的许多版本(或Docker术语中的标签)。
Docker镜像和容器
Docker镜像是一个包含运行应用程序所需的所有文件的软件包。这包括应用程序代码(如Laravel示例应用程序), 运行时间(如PHP), 系统库(如PHP所依赖的), 系统工具(如Apache等Web服务器), 和系统设置(如网络配置).
容器是一个隔离的环境,在其中执行镜像。虽然传统上不能依靠容器来执行虚拟机(VM)提供的那种隔离,但它们经常被用来在单个物理或虚拟机上并排执行受信任的代码。
容器有自己的隔离网络栈。这意味着默认情况下,在一个Docker容器中运行的代码不能通过网络与另一个Docker容器交互。为了让一个容器接触到网络流量,我们需要将一个主机端口映射到一个容器端口。这就是我们用-p 3306:3306 参数实现的,它将主机上的端口3306 映射到容器上的端口3306 。localhost 有了这个参数,通往3306 端口的流量就会被重定向到容器的3306 端口。
创建一个基本的PHP Docker镜像
我们已经看到了必须在我们的工作站上直接运行的命令来构建和运行Laravel应用程序。从概念上讲, 构建一个Docker镜像需要在一个叫做Dockerfile 的文件中编写这些相同的命令.
除了运行命令来下载依赖关系和执行测试外, 一个Dockerfile ,还包含了从主机复制文件到Docker镜像的命令,暴露网络端口,设置环境变量,设置工作目录,定义容器启动时默认运行的命令,等等。完整的指令清单可以在Docker文档中找到。
让我们看一个简单的例子。把下面的文字保存到你本地工作站某个方便的地方,名为Dockerfile 的文件:
FROM php:8
RUN echo "<?php\n"\
"print 'Hello World!';\n"\
"?>" > /opt/hello.php
CMD ["php", "/opt/hello.php"]
FROM 指令定义了我们的镜像将在其上构建的基础镜像。我们利用所有主要编程语言都提供官方支持的Docker镜像,其中包括所需的运行时间,在这个例子中,我们使用了来自DockerHub的php 镜像。冒号将镜像名称和标签分开,8 表示要使用的镜像的标签,或版本。
FROM php:8
RUN 指令在正在创建的镜像的上下文中执行一个命令。这里,我们呼应一个简单的PHP脚本,将Hello World! 打印到文件/opt/hello.php 。这在我们的Docker镜像中创建了一个新文件。
RUN echo "<?php\n"\
"print 'Hello World!';\n"\
"?>" > /opt/hello.php
CMD 指令配置了当基于此镜像的容器运行时要运行的命令。在这里,我们使用一个数组来建立命令的组成部分,从调用php 开始,然后将我们用先前的RUN 指令创建的文件传递给它。
CMD ["php", "/opt/hello.php"]
用命令建立一个图像。
docker build . -t phphelloworld
docker build 指令是用来构建新的Docker镜像的。句号表示Dockerfile 在当前工作目录下。-t phphelloworld 参数定义了新镜像的名称。
然后,用下面的命令创建一个基于该镜像的容器。
docker run phphelloworld
当PHP执行保存在/opt/hello.php 的脚本时,你会看到Hello World! 打印到控制台。
从现有的PHP应用程序创建一个Docker镜像的过程与这个简单的例子基本相同。应用程序的源代码文件被复制到Docker镜像中,而不是用echo 命令创建它们。安装了一些额外的工具以允许下载依赖项,并安装了额外的PHP模块以支持构建过程和数据库访问,但从根本上说,我们使用同样的指令来实现所需的结果。
让我们来看看我们如何将Laravel应用程序的样本容器化.
容器化Laravel应用程序
正如我们在上面的例子中所看到的, 在构建Docker镜像的过程中,我们几乎可以运行任何我们想要的命令。
作为构建应用程序的过程的一部分, 我们需要下载依赖性, 我们也将运行任何测试以确保正在构建的代码是有效的。这个应用程序中包含的测试需要访问一个初始化的数据库,作为镜像构建过程的一部分,我们必须满足这个要求。
运行应用程序也需要访问数据库,我们不能假设用于测试的数据库也是用于运行应用程序的同一数据库。因此,在应用程序运行之前,我们需要再次运行数据库迁移,以确保我们的应用程序拥有所需的数据。
让我们来看看一个Dockerfile ,它可以下载依赖项、初始化测试数据库、运行测试、配置应用程序最终要运行的数据库,然后启动应用程序,同时确保使用所需的PHP版本。
编写Docker文件
下面是一个保存在quickstart 目录中的Dockerfile 的例子,示例应用程序代码被检查出来:
FROM php:7
ENV PORT=8000
RUN apt-get update; apt-get install -y wget libzip-dev
RUN docker-php-ext-install zip pdo_mysql
RUN wget https://raw.githubusercontent.com/composer/getcomposer.org/master/web/installer -O - -q | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR /app
COPY . /app
RUN composer install
RUN touch /app/database/database.sqlite
RUN DB_CONNECTION=sqlite php artisan migrate
RUN DB_CONNECTION=sqlite vendor/bin/phpunit
RUN echo "#!/bin/sh\n" \
"php artisan migrate\n" \
"php artisan serve --host 0.0.0.0 --port \$PORT" > /app/start.sh
RUN chmod +x /app/start.sh
CMD ["/app/start.sh"]
让我们把这个文件分解一下。
与之前的 "Hello World!"例子一样,我们的这个Docker镜像是基于php 镜像的。然而,这一次,我们使用标签7 ,以确保基础镜像安装了PHP 7。如果你还记得,我们必须使用PHP 7,因为这个示例程序所使用的方法在PHP 8中已经废弃了。
值得花点时间来欣赏一下Docker使得在一台机器上并排构建和运行不同版本的PHP应用程序变得如此简单。
至少,在没有Docker的情况下,要运行多个版本的PHP,必须将PHP 7和8下载到不同的目录下,并记得在项目之间调用相应的可执行文件。你的操作系统的包管理器可能提供了安装多个版本的PHP的能力,但这样做的过程在Linux、macOS和Windows之间是不同的。
在Docker中,将特定的PHP版本与正在运行的代码相匹配的能力是一个固有的特性,因为每个Docker镜像都是一个独立的包,包含了运行一个应用程序所需的所有文件。
FROM php:7
ENV 指令定义了一个环境变量。在这里,我们定义了一个名为PORT 的变量,并给它一个默认值:8000 。我们将在启动Web服务器时使用这个变量。
ENV PORT=8000
php 基本镜像是基于Debian的,这意味着我们使用apt-get 进行软件包管理。在这里, 我们用apt-get update 来更新依赖列表, 然后安装wget 和zip开发库:
RUN apt-get update; apt-get install -y wget libzip-dev
我们的Laravel应用程序需要zip 和pdo_mysql PHP扩展来构建和运行。php 基础镜像包括一个名为docker-php-ext-install 的辅助工具,为我们安装PHP扩展:
RUN docker-php-ext-install zip pdo_mysql
为了构建应用程序, 我们需要Composer来下载这些依赖项.在这里,我们用wget 下载Composer安装脚本,并执行它来安装Composer到/usr/local/bin/composer:
RUN wget https://raw.githubusercontent.com/composer/getcomposer.org/master/web/installer -O - -q | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR 指令为任何后续指令设置工作目录,如RUN 或CMD 。这里,我们将工作目录设置为一个新的目录,称为/app :
WORKDIR /app
COPY 指令将主机中的文件复制到Docker镜像中。在这里,我们将当前目录中的所有文件复制到Docker镜像中的名为/app 的目录中:
COPY . /app
在这里,我们用composer 安装应用程序的依赖性:
RUN composer install
测试需要访问一个数据库来完成。为方便起见,我们将使用SQLite,它是一个本地内存数据库,需要创建一个文件来承载数据库表。
RUN touch /app/database/database.sqlite
因为项目中的测试需要一个初始化的数据库,我们将迁移脚本配置为使用SQLite并运行php artisan migrate ,以创建一个所需的表。
RUN DB_CONNECTION=sqlite php artisan migrate
然后我们运行代码中包含的任何测试。如果这些测试失败,构建Docker镜像的操作也会失败,确保我们只在测试成功通过的情况下构建一个镜像。
RUN DB_CONNECTION=sqlite vendor/bin/phpunit
如果我们已经达到了这一点, 测试已经通过, 我们现在配置镜像, 当容器启动时运行Laravel应用程序.这里的echo 命令写了一个脚本到/app/start.sh 。 脚本中的第一个命令是运行数据库迁移,第二个命令是启动开发的Web服务器。命令php artisan serve --host 0.0.0.0 --port=$PORT ,确保Web服务器监听所有可用的IP地址,这是Docker将主机上的端口映射到容器中并监听环境变量PORT 中定义的端口所需要的。
RUN echo "#!/bin/sh\n" \
"php artisan migrate\n" \
"php artisan serve --host 0.0.0.0 --port \$PORT" > /app/start.sh
我们刚刚创建的脚本文件需要被标记为可执行文件才能运行。
RUN chmod +x /app/start.sh
最后一条指令定义了基于此镜像的容器启动时要运行的命令。我们启动上面创建的脚本来初始化数据库并启动Web服务器。
CMD ["/app/start.sh"]
在我们构建镜像之前,我们需要在Dockerfile ,同时创建一个名为.dockerignore 的文件,内容如下。
vendor
.dockerignore 文件列出了将被排除在COPY . /app 等指令之外的文件和目录。在这里,我们指示Docker忽略任何可能被主机上的composer install 调用而下载的文件。这确保了在构建Docker镜像时,所有的依赖都是新下载的。
现在我们可以用下面的命令构建Docker镜像。
docker build . -t laravelapp
然后我们可以观察Docker执行Dockerfile 中定义的指令,复制和构建样本应用程序。
假设你仍然有MySQL Docker容器在运行, 用以下命令运行Laravel镜像.注意,数据库主机被设置为host.docker.internal 。这是一个特殊的主机名,由Docker在容器中暴露在Windows和macOS主机上,可解析为主机。这允许在容器中运行的代码与主机上运行的服务进行交互。
docker run -p 8001:8000 -e DB_HOST=host.docker.internal -e DB_DATABASE=laravel -e DB_USERNAME=laravel -e DB_PASSWORD=Password01! laravelapp
Linux用户不能访问host.docker.internal 主机名,而必须使用172.17.0.1 的IP地址。
docker run -p 8001:8000 -e DB_HOST=172.17.0.1 -e DB_DATABASE=laravel -e DB_USERNAME=laravel -e DB_PASSWORD=Password01! laravelapp
为了了解172.17.0.1 的IP地址来自哪里,运行以下命令:
docker network inspect bridge
这将显示关于bridge 网络的技术信息,这是我们一直在运行Docker容器的默认网络。下面的输出已经被剥离,以显示Gateway 的重要值,它定义了一个容器可以连接到主机的IP地址。正如你所看到的,这就是172.17.0.1 的来源。
[ { "IPAM": { "Config": [ { "Gateway": "172.17.0.1" ... } ... ]
},
...
}
]
我们用-p 8001:8000 ,将主机上的端口8001 映射到容器中的端口8000 。然后我们设置了一些环境变量来配置Laravel应用程序,以使用MySQL Docker容器。
打开http://localhost:8001,访问在Docker容器内运行的样本应用程序。
我们现在有了一个独立的Docker镜像,可以分发给其他开发者或承载Docker容器的平台。然而,我们刚刚创建的镜像只存在于我们的本地工作站上。在下一节中,我们将配置一个CI管道来自动构建这个镜像并将其发布到Docker仓库。
创建一个CI管线
有许多CI平台可以用来构建Docker镜像。在这篇文章中,我们将使用GitHub Actions。
GitHub Actions是内置于GitHub的一项服务,它提供了一个执行环境和插件生态系统,使我们能够轻松地将普通任务自动化,例如构建Docker镜像。
我们将把最终的镜像发布到一个叫DockerHub的Docker仓库。你可以在这里注册一个免费账户。免费账户允许你发布公共的Docker镜像,与任何人分享,使其成为开源应用程序的一个方便的选择。
我们需要在GitHub中把DockerHub的凭证作为秘密抓取。这可以通过打开GitHub仓库中的设置->秘密来完成。我们需要创建两个秘密:DOCKERHUB_USERNAME ,包含我们的DockerHub用户名,和DOCKERHUB_PASSWORD ,包含我们的DockerHub密码。

CI管道被定义在一个名为.github/workflows/main.yaml 的文件中。完整的代码如下所示。
name: CI
on:
push:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/laravelapp
build-args: |
db_host=172.17.0.1
db_username=laravel
db_database=laravel
db_password=Password01!
name 属性定义了工作流的名称。
name: CI
on 属性定义了工作流被触发的时间。子push 属性配置了这个工作流,以便在每次有变化被推送到git仓库时运行。
on:
push:
jobs 属性是定义一个或多个命名进程的地方。build 属性创建了一个同名的作业。
jobs:
build:
构建作业将在Ubuntu虚拟机上运行。
runs-on: ubuntu-latest
每个作业都由一些steps ,它们结合起来以达到一个共同的结果,在我们的例子中,就是构建和发布Docker镜像。
steps:
第一步是签出git仓库。
- uses: actions/checkout@v2
然后我们使用docker/setup-buildx-action动作来初始化构建Docker镜像的环境。
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
我们之前配置的凭证被用来登录DockerHub。
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
最后,我们使用docker/build-push-action动作来构建Docker镜像。通过将with.push 属性设置为true ,生成的镜像将被推送到DockerHub。
请注意,with.tags 属性被设置为${{ secrets.DOCKERHUB_USERNAME }}/laravelapp ,这将解析为类似mcasperson/laravelapp (其中mcasperson 是我的DockerHub用户名)。用用户名作为镜像名称的前缀,表明该镜像将被推送到的Docker注册中心。你可以在这里看到这个镜像被推送到我的DockerHub账户。
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/laravelapp
在GitHub中定义了这个工作流程后,对git仓库的每次提交都会触发Docker镜像的重建和发布,这就给了我们一个CI管道。
发布的镜像可以通过以下命令运行。用你自己的DockerHub用户名替换mcasperson ,以从你自己的公共仓库中提取镜像。
这里是Bash命令:
docker run \
-p 8001:8000 \
-e DB_HOST=172.17.0.1 \
-e DB_DATABASE=laravel \
-e DB_USERNAME=laravel \
-e DB_PASSWORD=Password01! \
mcasperson/laravelapp
这里是相应的PowerShell命令:
docker run `
-p 8001:8000 `
-e DB_HOST=172.17.0.1 `
-e DB_DATABASE=laravel `
-e DB_USERNAME=laravel `
-e DB_PASSWORD=Password01! `
mcasperson/laravelapp
你可以在这里找到包含上述GitHub Actions工作流程的样本应用程序仓库的分叉。
部署容器
如果说将应用程序容器化有什么好处的话,那就是可以将其部署到令人难以置信的平台范围。你可以将容器部署到Kubernetes或任何管理的Kubernetes平台,如Azure Kubernetes Service(AKS)、AWSElastic Kubernetes Service(EKS)或Google Kubernetes Engine(GKE)。Azure也有容器实例,而AWS有App Runner,谷歌有Cloud Run。
除了主要的云供应商外,你还可以将容器部署到Heroku、Dokku、Netlify等地。
在下面的截图中,我已经将图像部署到Azure容器实例上。这涉及到将容器实例指向公共Docker镜像,并设置一些环境变量来配置端口和数据库设置。只需点击几下,应用程序就被部署并可公开访问。


结论
在这篇文章中,我们介绍了一个容器化Laravel PHP应用程序的过程。通过对Laravel项目创建的一个旧的样本应用程序进行容器化,我们遇到了许多现实世界的问题,包括PHP版本的依赖性,PHP扩展,需要数据库访问的集成测试,数据库迁移脚本,以及初始化MySQL数据库等支持基础设施。
我们还介绍了一个持续集成的过程,用GitHub Actions工作流程的样本来构建和发布所产生的Docker镜像,并看到了如何将该镜像部署到支持Docker的众多托管平台之一。
最终的结果是,我们可以通过一个简单的调用docker ,来构建和测试我们的Laravel应用程序,而这又会生成一个Docker镜像,任何人都可以下载并运行一个或两个额外的docker 。这使得开发人员更容易使用这个项目, 运营人员更容易部署.