容器化您的 .NET Core 应用程序 – 正确的方法 常用方法 多阶段构建 您可以选择

2025-06-07

容器化你的 .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"]
Enter fullscreen mode Exit fullscreen mode

它本身并没有什么特别的问题。虽然可以运行,但根本没有进行任何调整或优化。无论是性能还是安全方面,它都不是生产环境的最佳选择。以下是一些示例:

  • 容器以 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"]
Enter fullscreen mode Exit fullscreen mode

此示例使用容器化构建。这意味着应用程序构建本身被移至 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"]
Enter fullscreen mode Exit fullscreen mode

让我们仔细看看各个步骤:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1 AS build-env
...
RUN dotnet publish \
  -c Release \
  -o ./output
...
Enter fullscreen mode Exit fullscreen mode

我们的第一阶段基于 SDK 映像,它提供了构建我们的应用程序所需的所有依赖项。

...
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
...
COPY --from=build-env /app/output .
...
Enter fullscreen mode Exit fullscreen mode

在第二阶段,我们定义一个新的基础镜像,这次它只包含运行时依赖项。然后,我们将第一阶段的应用程序构件复制到第二阶段。

有了这些,我们现在可以构建一个更小、更安全的容器镜像,因为它只包含执行应用程序所需的依赖项。但我们仍有进一步改进的空间,我们将在下一段讨论。

如果您想了解有关 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"]
Enter fullscreen mode Exit fullscreen mode

再次,我们仔细看看各个步骤:

ARG VERSION=3.1-alpine3.10
FROM mcr.microsoft.com/dotnet/core/sdk:$VERSION AS build-env
...
Enter fullscreen mode Exit fullscreen mode

我们首先使用 ARG 指令定义基础镜像标签。这有助于我们轻松更新标签,而无需更改几行代码。您可能已经注意到,我们使用了不同的标签。标签 3.1-alpine3.10表示此镜像包含 ASPNET 3.1 版本,并且基于 Alpine 3.10。

Alpine Linux 是一个专为安全性、简洁性和资源效率用例而设计的 Linux 发行版。在此阶段,Alpine Linux 已经可以帮助我们减少构建阶段的占用空间。

...
FROM mcr.microsoft.com/dotnet/core/aspnet:$VERSION
...
Enter fullscreen mode Exit fullscreen mode

由于我们使用的是多阶段构建,因此还需要定义最终阶段使用的镜像。我们将再次使用基于 Alpine 的 ASPNET 运行时作为基础镜像。如前所述,基于 Alpine 构建镜像可以让我们构建更小、更安全的容器镜像。

ADD /src/*.csproj .
RUN dotnet restore
ADD /src .
RUN dotnet publish \
  -c Release \
  -o ./output
Enter fullscreen mode Exit fullscreen mode

与上例不同,这次我们将构建过程拆分为多个部分。该dotnet restore命令使用 NuGet 来恢复依赖项以及项目文件中指定的项目特定工具。依赖项恢复本身也是dotnet pubish命令的一部分,但将其拆分开来使我们能够将依赖项构建到单独的镜像层中。这缩短了构建镜像所需的时间,并减少了下载大小,因为只有在依赖项发生变化时才会重建镜像层依赖项。

...
RUN adduser \
  --disabled-password \
  --home /app \
  --gecos '' app \
  && chown -R app /app
USER app
...
Enter fullscreen mode Exit fullscreen mode

为了确保应用程序运行时的安全,我们需要在没有任何 root 权限的情况下执行它们。因此,我们创建了一个新用户,并使用 USER 定义更改用户上下文。

...
ENV DOTNET_RUNNING_IN_CONTAINER=true \
  ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
...
Enter fullscreen mode Exit fullscreen mode

由于我们运行应用时无需任何 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"]
Enter fullscreen mode Exit fullscreen mode

我们再次仔细看看各个步骤:

...
RUN dotnet publish \
  --runtime alpine-x64 \
  --self-contained true \
  /p:PublishTrimmed=true \
  /p:PublishSingleFile=true \
  -c Release \
  -o ./output
...
Enter fullscreen mode Exit fullscreen mode

与上一个示例最大的区别在于,我们将构建一个独立的应用程序。提供该参数--self-contained true将强制构建将所有依赖项包含到应用程序构件中,其中包括 .NET Core 运行时。因此,我们还需要定义要在其中执行二进制文件的运行时。这可以通过该--runtime alpine-x64参数完成。

由于最终镜像需要进行尺寸优化,我们定义了一个/p:PubishTrimmed=true标志,用于建议构建过程不包含任何未使用的库。该/p:PublishSingleFile=true标志可以加快构建过程本身的速度。但缺点是,您必须预先定义动态加载的程序集,以确保所需的库不会被裁剪,从而导致镜像中不可用。更多详情,请点击此处

较小镜像的第二个缺点是代码更改会导致更大的变更。这是因为代码和运行时被打包在一个镜像层中。每次代码更改时,整个镜像层都需要重建,并重新分发到运行代码的系统中。

...
FROM mcr.microsoft.com/dotnet/core/runtime-deps:$VERSION
...
Enter fullscreen mode Exit fullscreen mode

由于应用程序构件是自包含的,我们无需为镜像提供运行时。在本例中,我选择了基于 Alpine Linux 的 Runtime-deps 镜像。该镜像已精简到只包含执行应用程序构件所需的最少原生依赖项。

...
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 \
  DOTNET_RUNNING_IN_CONTAINER=true \
  ASPNETCORE_URLS=http://+:8080
...
Enter fullscreen mode Exit fullscreen mode

另一个镜像大小改进方法是使用全球化不变模式。此模式适用于不具备全局感知能力的应用程序,这些应用程序可以使用不变文化的格式约定、大小写约定以及字符串比较和排序顺序。全球化不变模式可通过DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1环境变量启用。如果您的应用程序需要全球化,则需要安装ICU 库并移除上述环境变量。这将使您的容器镜像大小增加约 28 MB。您可以在此处找到有关全球化不变模式的更多详细信息

...
ENTRYPOINT ["./sample-mvc", "--urls", "http://0.0.0.0:8080"]
Enter fullscreen mode Exit fullscreen mode

对于独立应用程序,我们需要更改 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
PREV
React-query 系列第 1 部分:基本 react-query 设置
NEXT
VueJS 基础知识