Docker:多阶段构建 ASP.NET Core 应用镜像

今天我们一起来写 Dockerfile 构建一个 ASP.NET Core 应用镜像,同时还会将镜像发布到 Docker Hub 仓库。

1创建示例 Web 应用程序

为了演示,我们先创建一个 ASP.NET Core 应用程序:.

PS D:\Samples> dotnet new web -o AspNetDemo
已成功创建模板“ASP.NET Core Empty”。

正在处理创建后操作...
在 D:\Samples\AspNetDemo\AspNetDemo.csproj 上运行 “dotnet restore”...
  正在确定要还原的项目…
  已还原 D:\Samples\AspNetDemo\AspNetDemo.csproj (用时 77 ms)。
已成功还原。

项目创建好了,检查一下看看是否能正常运行:

PS D:\Samples> cd .\AspNetDemo\
PS D:\Samples\AspNetDemo> dotnet run
正在生成...
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7000
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5276
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Samples\AspNetDemo\

打开输出提示的地址 https://localhost:7000,浏览器正常显示 “Hellow World”,说明应用程序正常。

然后我们在 AspNetDemo 目录中添加一个 Dockerfile 文件,为下文作准备。

2依赖本地环境构建镜像

我们可以在 DockerHub 找到我们需要的 ASP.NET Core 运行时基础镜像:

Docker:多阶段构建 ASP.NET Core 应用镜像

现在,要把我们的 AspNetDemo 应用通过镜像的方式发布,利用我们之前学过的 Docker 知识,我们很自然会想到这样的思路:通过 dotnet publish 命令打包发布文件,然后把发布文件复制到 ASP.NET Core 运行时基础镜像中。

于是我们先在本地(bin/Publish 目录)生成好发布文件:

PS D:\Samples\AspNetDemo> dotnet publish -c release -o bin/Publish
  正在确定要还原的项目…
  所有项目均是最新的,无法还原。
  AspNetDemo -> D:\Samples\AspNetDemo\bin\release\net6.0\AspNetDemo.dll
  AspNetDemo -> D:\Samples\AspNetDemo\bin\Publish\

然后 Dockerfile 文件可以这样写:

FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY bin/Publish .
ENTRYPOINT ["dotnet", "AspNetDemo.dll"]

开始构建镜像:

PS D:\Samples\AspNetDemo> docker build -t aspnetdemo .
[+] Building 0.8s (8/8) FINISHED
...

PS D:\Samples\AspNetDemo> docker image ls
REPOSITORY           TAG           IMAGE ID       CREATED          SIZE
aspnetdemo           latest        62c8c40cbc70   33 seconds ago   208MB

试试在本地使用该镜像运行容器:

PS D:\Samples\AspNetDemo> docker run -d -p 80:80 aspnetdemo
a4d67637585c67384a6c7a3a9e8a39acc345253730ce22f39b7afdedec353397

打开浏览器访问 localhost 效果如下:

Docker:多阶段构建 ASP.NET Core 应用镜像

看起来还不错。

3多阶段构建镜像

相信很多童鞋已经想到了上面依赖本地的开发环境构建镜像存在的问题了。我们前面构建的 aspnetdemo 镜像,是先在本地生成好了发布的文件再复制到镜像里的。这样存在的一个明显问题是,其他人如果环境和我们的不一致,构建的镜像就可能是一个有问题的镜像,甚至直接构建失败。这种例子很常见,比如同一套代码,在你的机器上可以正常运行,因为环境不同(比如未安装指定的软件、未配置环境变量等),在同事机器上可能就运行不起来。

要避免这种情况,生成发布文件甚至是开发测试的过程,就不能依赖本地的开发环境来做了。即,我们要把生成发布文件的过程也放到 Dockerfile 中去做。

但由于 ASP.NET Core 运行时镜像不具有编译的能力,所以我们需要把基础镜像换成 .NET SDK 镜像。这样就可以了吗?这样也不是不可以,但是 .NET SDK 镜像会比 ASP.NET Core 运行时镜像大很多,我们可以比较一下:

PS D:\Samples\AspNetDemo> docker image ls
REPOSITORY                         TAG                IMAGE ID       CREATED             SIZE
mcr.microsoft.com/dotnet/sdk       6.0                d3863aa157b5   6 days ago          736MB
mcr.microsoft.com/dotnet/aspnet    6.0683c56113596   8 weeks ago         208MB

可以看到 .NET SDK 镜像比 ASP.NET Core 运行时镜像大了 500 多 MB,这显然会大大降低镜像发布的速度。

这时候我们就需要用到多阶段构建了,思路是把镜像的构建分成多个阶段,不同的阶段使用不同的基础镜像,前面的所有阶段都只是为最后一个阶段做准备,最终发布的也是最后一个阶段。

下面使用多阶段构建来改写 Dockerfile,参考如下:

# 阶段一:build
# 选择 SDK 镜像用于编译源码和生成发布文件
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /source
# 复制源代码
COPY *.csproj *.cs .
# 生成发布文件
RUN dotnet publish -c release -o /app

# 阶段二:final
# 使用 ASP.NET Core 运行时镜像
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS final
WORKDIR /app
# 从 build 阶段复制生成好的发布文件
COPY --from=build /app .
ENTRYPOINT ["dotnet", "AspNetDemo.dll"]

这个 Dockerfile 还可以继续优化,我将在下一节课讲镜像的优化时再改写它。

为了观察效果,我们稍微修改一下 Program.cs 中 Http 响应的内容:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "v2: Hello Docker!");

app.Run();

再次构建并运行容器:

PS D:\Samples\AspNetDemo> docker image build -t aspnetdemo .
[+] Building 5.8s (13/13) FINISHED
...

PS D:\Samples\AspNetDemo> docker image ls
REPOSITORY                    TAG             IMAGE ID       CREATED          SIZE
aspnetdemo                    latest          d6a596b1514d   55 seconds ago   208MB

PS D:\Samples\AspNetDemo> docker run -d -p 80:80 aspnetdemo
e9b9045299f5a3b7614cb3cee91b00ebe67a066b1f65eff46369fa1844b1d824

打开浏览器访问 localhost 验证一下效果:

Docker:多阶段构建 ASP.NET Core 应用镜像

可以看到我们虽然把生成发布文件的过程放到了 Dockerfile 中,但通过多阶段构建,最后构建出来的镜像也是 208M,和前面一样。

所以,我们可以把编译运行所需要的环境配置都写到 Dockerfile 中,这样可以保证任何一台机器都可以顺利构建镜像,且不管谁来构建,相同的源代码构建出来的镜像都是一样的。

4发布镜像

最后我们可以把构建好的镜像发布到自己的 Docker 仓库,这里以 Docker Hub 为例(实际生产环境请发布到自己的私有仓库)。

先在 Docker Hub 创建一个 Repositry:

Docker:多阶段构建 ASP.NET Core 应用镜像

推送镜像前,需要在本地登录一下:

PS D:\Samples\AspNetDemo> docker login
Authenticating with existing credentials...
Login Succeeded

然后给我们的镜像打上一个标签(默认是latest):

PS D:\Samples\AspNetDemo> docker tag aspnetdemo liamwang/aspnetdemo
# 也可以指定标签:docker tag aspnetdemo:latest liamwang/aspnetdemo:latest

然后推送到远程仓库:

PS D:\Samples\AspNetDemo> docker push liamwang/aspnetdemo
The push refers to repository [docker.io/liamwang/aspnetdemo]
e68e6a7d93c2: Pushed
ace5cec48f84: Pushed
17aff088b762: Pushed
9a515fdf7f03: Pushed
c4d9ca739af5: Pushed
3f94255da7c2: Pushed
608f3a074261: Pushed
latest: digest: sha256:dc479f2e52d48b3a81c0a83b5c740a085b299d046f268d21bb61c5bcfa5ae608 size: 1787
PS D:\Samples\AspNetDemo>

这步完成后,可以在 Docker Hub 上看到已发布的镜像:

Docker:多阶段构建 ASP.NET Core 应用镜像

然后我们可以到任意一台服务器 pull 该镜像运行容器了。

5小结

本节课我们以 ASP.NET Core 应用为例,先是用依赖本地环境的方式构建了镜像,分析了这种方式存在的问题,然后讲了如何使用多阶段构建来解决这个问题,最后演示了如何把已经构建好的镜像发布到自己的 Docker 镜像仓库。

下节课我们来解析和理解镜像的分层,理解镜像的分层可以帮助我们优化镜像的构建过程,也有助于制作更优质的镜像。