在 Visual Studio 中自定义 Docker 容器

通过编辑你向项目添加 Docker 支持时 Visual Studio 生成的 Dockerfile,可以自定义容器映像。 无论你是从 Visual Studio IDE 生成自定义容器,还是设置命令行生成,都需要了解 Visual Studio 是如何使用 Dockerfile 生成项目的。 你需要了解此类详细信息,因为出于性能方面的考虑,Visual Studio 在生成和运行容器化应用时遵循一个特殊的流程,而这在 Dockerfile 中并不明显。

假设你想要在 Dockerfile 中进行一项更改,并在调试和生产容器中查看结果。 在这种情况下,可以在 Dockerfile 中添加命令来修改第一个阶段(通常为 base)。 请参阅修改容器映像以用于调试和生产。 但是,如果你只想在调试时而不是生产时进行更改,则应创建另一个阶段,并使用 DockerfileFastModeStage 生成设置来告知 Visual Studio 将该阶段用于调试生成。 请参阅修改容器映像以仅用于调试

本文详细介绍了 Visual Studio 的容器化应用生成过程,然后介绍了有关如何修改 Dockerfile 以同时影响调试和生产生成或仅影响调试的信息。

Visual Studio 中的 Dockerfile 生成

注意

本部分介绍在你选择 Dockerfile 容器生成类型时 Visual Studio 使用的容器生成过程。 如果使用 .NET SDK 生成类型,自定义选项会有所不同,并且本部分中的信息不适用。 请改为参阅使用 dotnet publish 容器化 .NET 应用,并使用自定义容器中所述的属性来配置容器生成过程。

多阶段生成

Visual Studio 生成不使用 Docker 容器的项目时,它会在本地计算机上调用 MSBuild,并在本地解决方案文件夹下的文件夹(通常为 bin)中生成输出文件。 但是,对于容器化项目,生成过程会考虑有关生成容器化应用的 Dockerfile 说明。 Visual Studio 使用的 Dockerfile 分为多个阶段。 此过程依赖 Docker 的多阶段生成功能。

多阶段生成功能有助于使容器的生成过程更高效,并使容器更小,方法是让容器仅包含应用在运行时需要的位。 多阶段生成用于 .NET Core 项目,而不用于 .NET Framework 项目。

使用多阶段生成功能,可以在生成中间映像的阶段创建容器映像。 例如,考虑一个典型的 Dockerfile。 第一个阶段在 Visual Studio 生成的 Dockerfile 中称为 base,尽管这些工具并不需要该名称。

FROM mcr.microsoft.com/dotnet/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

Dockerfile 中的行以 Microsoft Container Registry (mcr.microsoft.com) 中的 ASP.NET 映像开头,创建可公开端口 80 和 443 的中间映像 base,并将工作目录设置为 /app

下一阶段是 build,其显示如下:

FROM mcr.microsoft.com/dotnet/sdk:3.1-buster-slim AS build
WORKDIR /src
COPY ["WebApplication43/WebApplication43.csproj", "WebApplication43/"]
RUN dotnet restore "WebApplication43/WebApplication43.csproj"
COPY . .
WORKDIR "/src/WebApplication43"
RUN dotnet build "WebApplication43.csproj" -c Release -o /app/build

你可以看到,build 阶段是从注册表(sdk 而不是 aspnet)中的其他原始映像开始,而不是从基础映像继续。 sdk 映像包含所有生成工具,因此,它比仅包含运行时组件的 aspnet 映像大得多。 如果看看 Dockerfile 的其余部分,你就会清楚使用单独映像的原因:

FROM build AS publish
RUN dotnet publish "WebApplication43.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication43.dll"]

最后阶段再次从 base 开始,并包括 COPY --from=publish 以将发布的输出复制到最终映像中。 由于无需包含 sdk 映像中的所有生成工具,因此此过程可以使最终映像小得多。

MSBuild

注意

本部分介绍如何在选择 Dockerfile 容器生成类型时自定义 Docker 容器。 如果使用 .NET SDK 生成类型,自定义选项会有所不同,并且本文中的信息不适用。 请改为参阅使用 dotnet publish 容器化 .NET 应用

由 Visual Studio for .NET Framework 项目(以及使用 Visual Studio 2017 Update 4 之前版本的 Visual Studio 版本创建的 .NET Core 项目)创建的 Dockerfile 不是多阶段 Dockerfile。 这些 Dockerfile 中的步骤不编译代码。 当 Visual Studio 生成 .NET Framework Dockerfile 时,它将首先使用 MSBuild 编译项目。 成功后,Visual Studio 会生成 Dockerfile,这只是将生成输出从 MSBuild 复制到生成的 Docker 映像中。 由于编译代码的步骤不包含在 Dockerfile 中,因此不能从命令行使用 docker build 生成 .NET Framework Dockerfile。 应使用 MSBuild 生成这些项目。

要为单个 Docker 容器项目生成映像,可以将 MSBuild 与 /t:ContainerBuild 命令选项一起使用。 此命令会告知 MSBuild 生成目标 ContainerBuild 而不是默认目标 Build。 例如:

MSBuild MyProject.csproj /t:ContainerBuild /p:Configuration=Release

从 Visual Studio IDE 生成解决方案时,你会看到类似于在“输出”窗口中看到的输出。 始终使用 /p:Configuration=Release,因为在 Visual Studio 使用多阶段生成优化的情况下,生成“调试”配置时产生的结果可能与预期不符。 请参阅调试

如果使用的是 Docker Compose 项目,请使用此命令生成映像:

msbuild /p:SolutionPath=<solution-name>.sln /p:Configuration=Release docker-compose.dcproj

调试

注意

本部分介绍如何在选择 Dockerfile 容器生成类型时自定义 Docker 容器。 如果使用 .NET SDK 生成类型,自定义选项会有所不同,并且本部分中的大部分信息不适用。 请改为参阅使用 dotnet publish 容器化 .NET 应用

在“调试”配置中生成时,Visual Studio 执行的一些优化操作有助于提高容器化项目生成过程的性能。 容器化应用的生成过程并不像仅遵循 Dockerfile 中所述的步骤那样简单。 在容器中生成比在本地计算机上生成要慢。 因此,在“调试”配置中生成时,Visual Studio 实际上会在本地计算机上生成项目,然后使用卷装载将输出文件夹共享到容器。 启用了此优化的生成称为“快速”模式生成。

在“快速”模式下,Visual Studio 使用参数调用 docker build,该参数指示 Docker 仅生成 Dockerfile 中的第一个阶段(通常是 base 阶段)。 可以通过设置 MSBuild 属性 DockerfileFastModeStage 来对此进行更改,如容器工具 MSBuild 属性所述。 Visual Studio 处理该过程的其余部分,而与 Dockerfile 的内容无关。 因此,修改 Dockerfile(例如自定义容器环境或安装其他依赖项)时,应将修改放在第一阶段。 不会执行 Dockerfile 的 buildpublishfinal 阶段中的任何自定义步骤。

仅当在“调试”配置中生成时才会进行此性能优化。 在“发布”配置中,生成在 Dockerfile 中指定的容器中进行。

如果要禁用性能优化并按 Dockerfile 指定的方式生成,请在项目文件中将“ContainerDevelopmentMode”属性设置为“常规”,如下所示 :

<PropertyGroup>
   <ContainerDevelopmentMode>Regular</ContainerDevelopmentMode>
</PropertyGroup>

若要恢复性能优化,请从项目文件中删除该属性。

启动调试 (F5) 时,将重新使用以前启动的容器(如有)。 如果不想重复使用以前的容器,则可以在 Visual Studio 中使用“Rebuild”或“Clean”命令强制 Visual Studio 使用新的容器 。

运行调试程序的过程取决于项目类型和容器操作系统:

方案 调试器进程
.NET Core 应用(Linux 容器) Visual Studio 下载 vsdbg 并将其映射到容器,然后通过程序和参数(即 dotnet webapp.dll)进行调用,接着调试程序附加到进程中。
.NET Core 应用(Windows 容器) Visual Studio 使用 onecoremsvsmon 并将其映射到容器,将其作为入口点运行,然后 Visual Studio 与其建立连接并附加到程序中。 此过程与通常在其他计算机或虚拟机上设置远程调试的过程类似。
.NET Framework 应用 Visual Studio 使用 msvsmon 并将其映射到容器,将其作为入口点(Visual Studio 可通过此入口点与其建立连接)的一部分运行,并附加到程序中。

有关 vsdbg.exe 的信息,请参阅通过 Visual Studio 对 Linux 和 OSX 上的 .NET Core 进行 Offroad 调试

修改容器映像以用于调试和生产

若要修改用于调试和生产的容器映像,请修改 base 阶段。 将自定义项添加到基础阶段部分中的 Dockerfile,通常是 Dockerfile 中的第一部分。 有关 Dockerfile 命令的信息,请参阅 Docker 文档中的 Dockerfile 参考

FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
# <add your commands here>

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["WebApplication3/WebApplication3.csproj", "WebApplication3/"]
RUN dotnet restore "WebApplication3/WebApplication3.csproj"
COPY . .
WORKDIR "/src/WebApplication3"
RUN dotnet build "WebApplication3.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "WebApplication3.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication3.dll"]

修改容器映像以仅用于调试

如果你要对容器执行某些操作来帮助你完成调试过程,例如出于诊断目的安装某些内容,但不希望将其安装在生产生成中,则此方案适用。

若要修改容器以仅用于调试,请创建一个阶段,然后使用 MSBuild 属性 DockerfileFastModeStage 告知 Visual Studio 在调试时使用自定义的阶段。 有关 Dockerfile 命令的信息,请参阅 Docker 文档中的 Dockerfile 参考

在以下示例中,我们安装 procps-ng 包,但仅在调试模式下安装。 此包会提供 pidof 命令,Visual Studio 需要该命令,但不在此处使用的 Mariner 映像中。 我们用于快速模式调试的阶段是 debug,即此处定义的自定义阶段。 快速模式阶段不需要从 buildpublish 阶段继承,它可以直接从 base 阶段继承,因为 Visual Studio 将装载一个卷,其中包含了运行应用所需的所有内容,如本文前面所述。

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.

FROM mcr.microsoft.com/dotnet/aspnet:6.0-cbl-mariner2.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

FROM base AS debug
RUN tdnf install procps-ng -y

FROM mcr.microsoft.com/dotnet/sdk:6.0-cbl-mariner2.0 AS build
WORKDIR /src
COPY ["WebApplication1/WebApplication1.csproj", "WebApplication1/"]
RUN dotnet restore "WebApplication1/WebApplication1.csproj"
COPY . .
WORKDIR "/src/WebApplication1"
RUN dotnet build "WebApplication1.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "WebApplication1.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication1.dll"]

在项目文件中,添加此设置,告知 Visual Studio 在调试时使用自定义的阶段 debug

  <PropertyGroup>
     <!-- other property settings -->
     <DockerfileFastModeStage>debug</DockerfileFastModeStage>
  </PropertyGroup>

接下来的部分包含在某些情况下可能有用的信息,例如,当你想要指定不同的入口点时,或者如果你的应用启用了 SSL,并且你正在更改可能会影响 SSL 证书处理方式的某些内容。

从命令行生成

如果要在 Visual Studio 外使用 Dockerfile 生成容器项目,可以使用 docker buildMSBuilddotnet builddotnet publish 从命令行生成。

如果使用 .NET SDK 生成类型,并且你没有 Dockerfile,则不能使用 docker build;请改用 MSBuilddotnet builddotnet publish 在命令行上生成。

使用 Docker 生成

要从命令行生成容器化解决方案,通常可以对解决方案中的每个项目使用命令 docker build <context>。 提供生成上下文参数。 Dockerfile 的生成上下文是本地计算机上的一个文件夹,用作生成映像的工作文件夹。 例如,将文件复制到容器时,它是要复制的文件所在的文件夹。 在 .NET Core 项目中,请使用包含解决方案文件 (.sln) 的文件夹。 此参数表示为相对路径,对于项目文件夹中的 Dockerfile 及其父文件夹中的解决方案文件,此参数通常为“...”。 对于 .NET Framework 项目,生成上下文为项目文件夹,而不是解决方案文件夹。

docker build -f Dockerfile ..

项目预热

项目预热指的是在为项目选择 Docker 配置文件时(即在加载项目或添加 Docker 支持时)执行的一系列步骤,旨在提高后续运行的性能(F5 或 Ctrl+F5) 。 在“工具”>“选项”>“容器工具”下可以配置此行为。 下面是在后台运行的任务:

  • 检查 Docker Desktop 是否已安装且正在运行。
  • 确保将 Docker Desktop 设置为与项目相同的操作系统。
  • 在 Dockerfile 的第一阶段(大部分 Dockerfile 中的 base 阶段)拉取映像。
  • 生成 Dockerfile 并启动容器。

预热仅在“快速”模式下发生,因此正在运行的容器对 app 文件夹进行卷装载。 这意味着对应用所做的任何更改都不会使容器无效。 此行为可以大幅提升调试性能,并缩短长期任务(例如拉取大型映像)的等待时间。

卷映射

为了能够在容器中正常调试,Visual Studio 使用卷映射从主机映射调试程序和 NuGet 文件夹。 有关卷映射,可参阅此处的 Docker 文档。 可以使用 Visual Studio 中的“容器”窗口查看容器的卷映射。

下面是在容器中装载的卷:

音量 说明
应用文件夹 包含 Dockerfile 所在的项目文件夹。
NuGet 包文件夹 包含从项目的 obj{project}.csproj.nuget.g.props 文件中读取的 NuGet 包和 fallback 文件夹。
远程调试程序 包含在容器中运行调试器所需的位,视具体项目类型而定。 请参阅调试部分。
源文件夹 包含传递给 Docker 命令的生成上下文。

下面是在容器中装载的卷。 在容器中看到的内容可能因使用的 Visual Studio 2022 的次要版本而异。

音量 说明
应用文件夹 包含 Dockerfile 所在的项目文件夹。
HotReloadAgent 包含用于热重载代理的文件。
HotReloadProxy 包含运行服务所需的文件,该服务使主机重新加载代理能够与主机上的 Visual Studio 通信。
NuGet 包文件夹 包含从项目的 obj{project}.csproj.nuget.g.props 文件中读取的 NuGet 包和 fallback 文件夹。
远程调试程序 包含在容器中运行调试器所需的位,视具体项目类型而定。 调试部分会对此进行详细说明。
源文件夹 包含传递给 Docker 命令的生成上下文。
TokenService.Proxy 包含运行服务所需的文件,该服务使 VisualStudioCredential 能够与主机上的 Visual Studio 通信。

对于 .NET 8,根位置以及包含用户机密和 HTTPS 证书的应用用户可能也存在其他装入点。 在 Visual Studio 17.10 预览版中,热重载和令牌服务卷以及另一个组件(无分发版帮助程序)组合在单个装入点 /VSTools 下。

注意

Visual Studio 17.10 预览版如果在适用于 Linux 的 Windows 子系统 (WSL) 中使用 Docker 引擎而不使用 Docker Desktop,请设置环境变量 VSCT_WslDaemon=1 以便以让 Visual Studio 在创建卷装载时使用 WSL 路径。 还需要使用 NuGet 包 Microsoft.VisualStudio.Azure.Containers.Tools.Targets 1.20.0-Preview 1

对于 ASP.NET Core Web 应用,SSL 证书和用户机密可能还有两个额外的文件夹,下一部分将对此进行详细介绍。

启用详细的容器工具日志

出于诊断目的,可以启用某些容器工具日志。 可以通过设置某些环境变量来启用这些日志。 对于单个容器项目,环境变量为 MS_VS_CONTAINERS_TOOLS_LOGGING_ENABLED,随后会记录到 %tmp%\Microsoft.VisualStudio.Containers.Tools 中。 对于 Docker Compose 项目,环境变量为 MS_VS_DOCKER_TOOLS_LOGGING_ENABLED,随后会记录在 %tmp%\Microsoft.VisualStudio.DockerCompose.Tools 中。

注意

启用日志记录并使用令牌代理进行 Azure 身份验证时,身份验证凭据可以记录为纯文本。 请参阅配置 Azure 身份验证

容器入口点

Visual Studio 使用自定义容器入口点,具体视项目类型和容器操作系统而定,下面是不同的组合:

容器类型 入口点
Linux 容器 入口点为 tail -f /dev/null,表示无限等待,可使容器保持运行。 当应用通过调试程序启动时,调试程序会负责运行应用(即 dotnet webapp.dll)。 如果在不进行调试的情况下启动,则该工具将运行 docker exec -i {containerId} dotnet webapp.dll 以运行应用。
Windows 容器 入口点类似于用于运行调试程序的 C:\remote_debugger\x64\msvsmon.exe /noauth /anyuser /silent /nostatus,因此它侦听连接。 当调试程序运行应用时,此方法适用。 在没有调试的情况下启动时,将使用 docker exec 命令。 对于 .NET Framework Web 应用,入口点略有不同,会向命令添加 ServiceMonitor

容器入口点只能在 Docker Compose 项目中修改,而不能在单个容器项目中修改。

启用 SSL 的 ASP.NET Core 应用

Visual Studio 中的容器工具支持使用开发证书调试启用 SSL 的 ASP.NET Core 应用,调试方式与在没有容器的情况下相同。 为实现此目的,Visual Studio 会添加几个步骤来导出证书,使其可供容器使用。 下面是 Visual Studio 在容器中进行调试时处理的流:

  1. 通过 dev-certs 工具,确保主机上存在本地开发证书且该证书受主机信任。

  2. 使用存储在此特定应用的用户机密存储中的安全密码将该证书导出到 %APPDATA%\ASP.NET\Https。

  3. 卷装载以下目录:

    • %APPDATA%\Microsoft\UserSecrets
    • %APPDATA%\ASP.NET\Https

ASP.NET Core 将查找与 Https 文件夹下的程序集名称匹配的证书,这就是将其映射到该路径中的容器的原因。 还可以使用环境变量(即 ASPNETCORE_Kestrel__Certificates__Default__PathASPNETCORE_Kestrel__Certificates__Default__Password)或在用户机密 json 文件中定义证书路径和密码,例如:

{
  "Kestrel": {
    "Certificates": {
      "Default": {
        "Path": "c:\\app\\mycert.pfx",
        "Password": "strongpassword"
      }
    }
  }
}

如果配置同时支持容器化和非容器化生成,则你应使用环境变量,因为路径是特定于容器环境的。

要详细了解如何将 SSL 与容器中的 ASP.NET Core 应用结合使用,请参阅 Hosting ASP.NET Core images with Docker over HTTPS(通过 HTTPS 使用 Docker 托管 ASP.NET Core 映像)。

如需代码示例来演示如何为主机和容器中受信任的多服务应用创建自定义证书来进行 HTTPS 服务到服务通信,请参阅 CertExample