容器化你的 .NET Core 应用——正确的方法
常见的方法
多阶段构建
你有选择
目前已经有很多文章详细介绍了如何容器化 .NET Core 应用程序。尽管如此,我仍然觉得有必要撰写一篇更详细的文章,帮助您基于容器和 .NET Core 最佳实践构建可用于生产的容器镜像。
为了更好地理解,我将基于一个小型 ASPNET Core Web 应用程序示例详细解释所有内容。您可以在此处找到有关该应用程序本身的更多详细信息。当然,共享的最佳实践不仅限于 .NET Core。您可以调整它们并将其应用于任何项目。
用例数以百万计,因此没有一个万能的解决方案。我想向您介绍我最常用的两种方案。您将获得所有详细信息,从而决定哪种方案最适合您。我们先从基础知识开始。
常见的方法
这是一个常见的 Dockerfile 示例,我经常遇到它:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY /app/output .
EXPOSE 8080
ENTRYPOINT ["dotnet", "sample-mvc.dll"]
它本身并没有什么特别的问题。虽然可以运行,但根本没有进行任何调整或优化。无论是性能还是安全方面,它都不是生产环境的最佳选择。以下是一些示例:
- 容器以 root 身份执行其进程
- 由于无效的图像层缓存,排序顺序效率低下,导致构建时间变慢
- 不适当的图像层管理会影响最终图像大小
- 由于缺少
.dockerignore
文件导致构建速度变慢
让我们仔细看看另一个 Dockerfile 示例:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1
WORKDIR /app
ADD /src .
RUN dotnet publish \
-c Release \
-o ./output
EXPOSE 8080
ENTRYPOINT ["dotnet", "sample-mvc.dll"]
此示例使用容器化构建。这意味着应用程序构建本身被移至 Docker 构建流程。这是一种非常好的模式,它允许您在一个不可变且隔离的构建环境中构建,并构建所有依赖项。但缺点是,您需要基于更大的 SDK 镜像来构建镜像。SDK 镜像提供了构建应用程序所需的依赖项,但之后执行应用程序时不需要它。幸运的是,有一个解决方案可以解决这个问题。
多阶段构建
如果您使用的是上述 Dockerfile 的类似版本,您可能没有听说过多阶段构建功能。多阶段构建允许我们将镜像构建过程拆分为多个阶段。
第一阶段用于构建需要提供所需依赖项的应用程序。在第二阶段,我们将应用程序构件复制到一个较小的运行时环境中,然后将其用作最终镜像。相应的 Dockerfile 可能如下所示:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
WORKDIR /app
ADD /src .
RUN dotnet publish \
-c Release \
-o ./output
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
WORKDIR /app
COPY --from=build-env /app/output .
EXPOSE 8080
ENTRYPOINT ["dotnet", "sample-mvc.dll"]
让我们仔细看看各个步骤:
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
...
RUN dotnet publish \
-c Release \
-o ./output
...
我们的第一阶段基于 SDK 映像,它提供了构建我们的应用程序所需的所有依赖项。
...
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
...
COPY --from=build-env /app/output .
...
在第二阶段,我们定义一个新的基础镜像,这次它只包含运行时依赖项。然后,我们将第一阶段的应用程序构件复制到第二阶段。
有了这些,我们现在可以构建一个更小、更安全的容器镜像,因为它只包含执行应用程序所需的依赖项。但我们仍有进一步改进的空间,我们将在下一段讨论。
如果您想了解有关 Dockerfile 最佳实践的更多信息,我建议您查看官方 Docker 文档的此页面。
你有选择
如上所述,最佳实践并非单一的。最佳实践因用例而异。通过以下示例,您将获得两个蓝图及其优缺点,然后您可以根据自己的需求进行调整。
一个好的起点
下面的 Dockerfile 是上述多阶段示例的优化版本,应该适合大多数场景。
ARG VERSION=3.1-alpine3.10
FROM mcr.microsoft.com/dotnet/core/sdk:$VERSION AS build-env
WORKDIR /app
ADD /src/*.csproj .
RUN dotnet restore
ADD /src .
RUN dotnet publish \
-c Release \
-o ./output
FROM mcr.microsoft.com/dotnet/core/aspnet:$VERSION
RUN adduser \
--disabled-password \
--home /app \
--gecos '' app \
&& chown -R app /app
USER app
WORKDIR /app
COPY --from=build-env /app/output .
ENV DOTNET_RUNNING_IN_CONTAINER=true \
ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "sample-mvc.dll"]
再次,我们仔细看看各个步骤:
ARG VERSION=3.1-alpine3.10
FROM mcr.microsoft.com/dotnet/core/sdk:$VERSION AS build-env
...
我们首先使用 ARG 指令定义基础镜像标签。这有助于我们轻松更新标签,而无需更改几行代码。您可能已经注意到,我们使用了不同的标签。标签 3.1-alpine3.10表示此镜像包含 ASPNET 3.1 版本,并且基于 Alpine 3.10。
Alpine Linux 是一个专为安全性、简洁性和资源效率用例而设计的 Linux 发行版。在此阶段,Alpine Linux 已经可以帮助我们减少构建阶段的占用空间。
...
FROM mcr.microsoft.com/dotnet/core/aspnet:$VERSION
...
由于我们使用的是多阶段构建,因此还需要定义最终阶段使用的镜像。我们将再次使用基于 Alpine 的 ASPNET 运行时作为基础镜像。如前所述,基于 Alpine 构建镜像可以让我们构建更小、更安全的容器镜像。
ADD /src/*.csproj .
RUN dotnet restore
ADD /src .
RUN dotnet publish \
-c Release \
-o ./output
与上例不同,这次我们将构建过程拆分为多个部分。该dotnet restore
命令使用 NuGet 来恢复依赖项以及项目文件中指定的项目特定工具。依赖项恢复本身也是dotnet pubish
命令的一部分,但将其拆分开来使我们能够将依赖项构建到单独的镜像层中。这缩短了构建镜像所需的时间,并减少了下载大小,因为只有在依赖项发生变化时才会重建镜像层依赖项。
...
RUN adduser \
--disabled-password \
--home /app \
--gecos '' app \
&& chown -R app /app
USER app
...
为了确保应用程序运行时的安全,我们需要在没有任何 root 权限的情况下执行它们。因此,我们创建了一个新用户,并使用 USER 定义更改用户上下文。
...
ENV DOTNET_RUNNING_IN_CONTAINER=true \
ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
...
由于我们运行应用时无需任何 root 权限,因此需要将其暴露在高于 1024 的端口上。在本例中,我们选择了 8080。通过 ENV 定义,我们将向应用进程暴露更多环境变量。DOTNET_RUNNING_IN_CONTAINER =true只是一个非正式的环境变量,用于告知开发人员/应用程序该进程正在容器中运行。ASPNETCORE_URLS =http://+:8080用于向运行时提供在 8080 端口上暴露进程的信息。
更小,更小,更小
如前所述,上述示例应该适用于大多数场景。以下示例描述了一种构建尽可能小的容器镜像的方法。一个可能的用例可能是用于IoT Edge 用例或需要优化启动时间的环境。遗憾的是,它也存在一些缺点,我将在下面详细讨论。
ARG VERSION=3.1-alpine3.10
FROM mcr.microsoft.com/dotnet/core/sdk:$VERSION AS build-env
WORKDIR /app
ADD /src .
RUN dotnet publish \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true \
-c Release \
-o ./output
FROM mcr.microsoft.com/dotnet/core/runtime-deps:$VERSION
RUN adduser \
--disabled-password \
--home /app \
--gecos '' app \
&& chown -R app /app
USER app
WORKDIR /app
COPY --from=build-env /app/output .
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
DOTNET_RUNNING_IN_CONTAINER=true \
ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["./sample-mvc", "--urls", "http://0.0.0.0:8080"]
我们再次仔细看看各个步骤:
...
RUN dotnet publish \
--runtime alpine-x64 \
--self-contained true \
/p:PublishTrimmed=true \
/p:PublishSingleFile=true \
-c Release \
-o ./output
...
与上一个示例最大的区别在于,我们将构建一个独立的应用程序。提供该参数--self-contained true
将强制构建将所有依赖项包含到应用程序构件中,其中包括 .NET Core 运行时。因此,我们还需要定义要在其中执行二进制文件的运行时。这可以通过该--runtime alpine-x64
参数完成。
由于最终镜像需要进行尺寸优化,我们定义了一个/p:PubishTrimmed=true
标志,用于建议构建过程不包含任何未使用的库。该/p:PublishSingleFile=true
标志可以加快构建过程本身的速度。但缺点是,您必须预先定义动态加载的程序集,以确保所需的库不会被裁剪,从而导致镜像中不可用。更多详情,请点击此处。
较小镜像的第二个缺点是代码更改会导致更大的变更。这是因为代码和运行时被打包在一个镜像层中。每次代码更改时,整个镜像层都需要重建,并重新分发到运行代码的系统中。
...
FROM mcr.microsoft.com/dotnet/core/runtime-deps:$VERSION
...
由于应用程序构件是自包含的,我们无需为镜像提供运行时。在本例中,我选择了基于 Alpine Linux 的 Runtime-deps 镜像。该镜像已精简到只包含执行应用程序构件所需的最少原生依赖项。
...
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
DOTNET_RUNNING_IN_CONTAINER=true \
ASPNETCORE_URLS=http://+:8080
...
另一个镜像大小改进方法是使用全球化不变模式。此模式适用于不具备全局感知能力的应用程序,这些应用程序可以使用不变文化的格式约定、大小写约定以及字符串比较和排序顺序。全球化不变模式可通过DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1环境变量启用。如果您的应用程序需要全球化,则需要安装ICU 库并移除上述环境变量。这将使您的容器镜像大小增加约 28 MB。您可以在此处找到有关全球化不变模式的更多详细信息。
...
ENTRYPOINT ["./sample-mvc", "--urls", "http://0.0.0.0:8080"]
对于独立应用程序,我们需要更改 ENTRYPOINT 定义以运行二进制文件本身。
此镜像的大小约为 73 MB(包括我的示例应用程序)。让我们将其与其他镜像进行比较:
- 基于通用多阶段 Dockerfile 的镜像:250 MB
- 基于上述多阶段 Dockerfile 的镜像:124 MB
正如上面提到的:哪个 Dockerfile 最适合你取决于你的用例。Dockerfile 越小并不一定越好。
如果您计划将应用程序部署到 Azure Kubernetes 服务或 Azure 容器实例,您可能还会考虑将镜像存储在 Azure 容器注册表中。Azure 容器注册表还支持构建容器镜像。您可以将其包含在构建管道中,也可以使用az acr build
命令手动调用它。
希望这些细节能帮助您容器化您的 .NET Core 应用程序。如前所述,以上示例均为最佳实践,可能需要根据您的需求进行定制。请查看Michael Dimoudis 的文章,了解如何强化您的 .NET Core 容器镜像。
文章来源:https://dev.to/nmeisenzahl/containerize-your-net-core-app-the-right-way-2ceh