实践 / ASP.NET Core 项目在 Linux 容器上开发、打包与部署全程

  • <零> 必要的工具
  • <一> 创建 DemoWeb 项目
  • <二> 添加 Dockerfile
  • <三> 了解 Dockerfile
  • <四> 容器工具与预热
  • <五> 生成与调试
  • <六> 发布 DemoWeb 镜像
  • <七> 反向代理服务器 nginx 与镜像构建
  • <八> 使用 docker-compose
  • <九> 服务器端部署
  • <十> 结语
  • <十一> 参考资料

容器技术因其众多的优点以及 DevOps 的流行变得越来越重要,本篇演示如何将一个 Core 项目构建成 Linux 平台的镜像并发布到服务器中,并介绍一些本人踩过的坑。理解本文需要一定的容器基础知识。

本文不包含 DevOps,持续集成或者持续交付的内容,整个流程设计主要是用于了解 Docker 技术以及如何将  Core 项目集成到 Linux 容器中。

<零> 必要的工具

这里使用的操作系统是 Windows 10 专业版 1909 ,开发工具是 Visual Studio Community 2019 ,当然还有必要的 Docker Desktop for Windows。

Visual Studio Community 2019 16.7.0

Docker Desktop for Windows 2.3.0.3(45519)

【可能的坑】
要成功安装 Docker Desktop for Windows 并成功使用 Linux 容器需要两个前提条件: 1、启用 CPU 的虚拟化功能。2、安装 Hyper-V 服务并能成功启动。

<一> 创建 DemoWeb 项目

这里使用一个  Core 的空项目模板作为演示,创建项目的时候先不要选择【启动 Docker 支持】。

创建空项目用于演示

<二> 添加Dockerfile

创建好项目之后可以进行生成以确认项目一切正常,然后右键点击项目在添加菜单中选择 【Docker 支持...】

在弹出窗口中选择目标 OS 为 Linux。

点击确定后 Visual Studio 会为项目添加一个 Dockerfile 文件。

<三> 了解 Dockerfile

Docker 技术使用 Dockerfile 文件里的指令来定义构建容器镜像(image)的过程,然后 Docker 使用 docker build 命令来执行镜像的构建。

Docker 引擎使用镜像来启动容器,一个典型的容器包含容器的操作系统以及运行在其上的应用,因此 Dockerfile 的里的指令首先就是要定义一个基础镜像(当然你也可以使用 FROM scratch 来从零开始构建一个镜像),这个镜像通常是一个操作系统,比如本例中的 aspnet:3.1-buster-slim 就是一个已经安装了  core 运行时的 Debian 10.5 "buster" 操作系统,其中的 slim 指的是这个操作系统是一个专门为容器定制的“瘦身”版本。

除了基础镜像, Dockerfile 还需要为镜像添加应用,这个应用可以是直接拷贝到镜像内的或者在镜像内部生成的,本例就是使用了一个包含了 .net core sdk 的基础镜像来生成我们的应用,然后将最终生成的内容拷贝到最终的基础镜像中。

最后 Dockerfile 还要为镜像设置一个入口命令行(ENTRYPOINT)作为镜像容器启动后执行的命令,这类似于编程语言里的入口函数。

下面就是项目中添加的默认 Dockerfile,每个步骤都做了注释。

#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
#引入aspnet:3.1-buster-slim镜像并重命名为 base
FROM http://mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
#设置base镜像的工作目录为/app
WORKDIR /app
#开放容器的80与443端口
EXPOSE 80
EXPOSE 443

#引入sdk:3.1-buster镜像并重命名为build
#该镜像用于在Linux平台上生成我们的项目并不包含在最终镜像中
FROM http://mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
#设置build镜像的工作目录为/src
WORKDIR /src
#拷贝项目文件DemoWeb.csproj到/src/DemoWeb/目录
COPY ["DemoWeb/DemoWeb.csproj", "DemoWeb/"]
#运行dotnet restore命令还原项目的依赖
RUN dotnet restore "DemoWeb/DemoWeb.csproj"
#拷贝Dockerfile所在目录的内容到/src目录
COPY . .
#设置工作目录为build镜像的/src/DemoWeb
WORKDIR "/src/DemoWeb"
#运行 dotnet build 生成命令,并将输出内容保存到/app/build目录
RUN dotnet build "DemoWeb.csproj" -c Release -o /app/build

FROM build AS publish
#运行 dotnet publish 发布命令,并将输入内容保存到/app/publish目录
RUN dotnet publish "DemoWeb.csproj" -c Release -o /app/publish

#使用base镜像最为最终的基础镜像
FROM base AS final
WORKDIR /app
#将publish镜像中/app/publish目录的内容拷贝到当前的/app目录
COPY --from=publish /app/publish .
#设置该镜像的入口命令为 dotnet DemoWeb.dll
ENTRYPOINT ["dotnet", "DemoWeb.dll"]
【可能的坑和建议】
这里的 Dockerfile 使用的镜像 Tag 里的版本号只有两位,所以在小版本号更新后,如果镜像仓库中对应 Tag 的镜像更新了,你将需要重新 pull 该镜像,这会导致重新下载,由于 mcr.microsoft.com 的镜像拉取非常慢,所以在你并不需要更新的时候这会很麻烦。
所以个人推荐在 FROM 镜像的时候使用更确切的版本以确保之后不会在不必要的时候更新。如下面的 Dockerfile 所示将 aspnet:3.1-buster-slim 改为 aspnet:3.1.7-buster-slim,以及 sdk:3.1-buster 改为 sdk:3.1.401-buster 以确保在后续构建中这些基础镜像不会自动更新。查阅这些具体 tag 清单的页面的链接可以在这些镜像的 Docker hub 主页中找到:

以下就是使用了确切版本号镜像 tag 的 Dockerfile,

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

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

FROM http://mcr.microsoft.com/dotnet/core/sdk:3.1.401-buster AS build
WORKDIR /src
COPY ["DemoWeb/DemoWeb.csproj", "DemoWeb/"]
RUN dotnet restore "DemoWeb/DemoWeb.csproj"
COPY . .
WORKDIR "/src/DemoWeb"
RUN dotnet build "DemoWeb.csproj" -c Release -o /app/build

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

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

<四> 容器工具与预热

在你添加 Dockerfile 后,Visual Studio 会立即启动容器工具并进行容器的预热。

具体步骤如下图所示,容器工具会开始一系列的检查,检查完成后会开始拉取 Dockerfile 中指定的镜像。

容器检查与预热步骤

<五> 生成与调试

预热完成后就可以点击调试按钮启动 Docker 模式下的调试了。

点击调试后我们可以在输出内容中看到生成的过程,

生成过程输出内容

整个启动过程中最重要的就是 docker run 命令,那么我们就来解析一下 docker run 这行命令,这样可以更好的理解 Visual Studio 是如何启动容器并调试的。

docker run -dt 
-v "C:\Users\wangl\vsdbg\vs2017u5:/remote_debugger:rw" 
-v "F:\Work\DemoWeb\DemoWeb:/app" 
-v "F:\Work\DemoWeb:/src/" 
-v "C:\Users\wangl\AppData\Roaming\Microsoft\UserSecrets:/root/.microsoft/usersecrets:ro" 
-v "C:\Users\wangl\AppData\Roaming\ASP.NET\Https:/root/.aspnet/https:ro" 
-v "C:\Users\wangl\.nuget\packages\:/root/.nuget/fallbackpackages2" 
-v "C:\Program Files\dotnet\sdk\NuGetFallbackFolder:/root/.nuget/fallbackpackages" 
-e "DOTNET_USE_POLLING_FILE_WATCHER=1" 
-e "ASPNETCORE_LOGGING__CONSOLE__DISABLECOLORS=true" 
-e "ASPNETCORE_ENVIRONMENT=Development" 
-e "ASPNETCORE_URLS=https://+:443;http://+:80" 
-e "NUGET_PACKAGES=/root/.nuget/fallbackpackages2" 
-e "NUGET_FALLBACK_PACKAGES=/root/.nuget/fallbackpackages;/root/.nuget/fallbackpackages2" 
-P 
--name DemoWeb_1 
--entrypoint tail 
demoweb:dev 
-f /dev/null

在这行命令中:

  • -v 指定容器的 volume 挂载,以第一个 -v 参数为例,就是将主机的C:\Users\wangl\vsdbg\vs2017u5 目录挂载到容器的 /remote_debugger 目录下,这些目录中的大多内容都是用于支持调试的。
  • -e 用于设置容器内操作系统的环境变量。
  • -P 发布(映射)容器端口到本地主机端口。
  • --entrypoint 覆盖容器本身的 entrypoint 为 tail 。
  • demoweb:dev 为镜像名称。
  • -f /dev/null 为传递给之前指定 entrypoint 的参数。

成功启动调试后可以在 Visual Studio 的底部看到如下图的信息,这里显示了容器以及容器内部的信息。

调试时的容器信息面板

其中,

  • 环境页显示的是容器操作系统的环境变量。
  • 端口页显示的是容器当前公开的端口以及与主机的端口映射。
  • 日志页显示的是应用输出的日志信息。
  • 文件页显示了容器操作系统的目录,你可以在这里查看容器操作系统里的所有文件。

启动调试成功后就可以看到网页了,这里的 32770 端口是 Visual Studio 调试工具在启动时绑定到容器的 80 端口的。

项目主页
【可能的坑】
在启动调试的过程中,如果调试一直无法启动并在输出信息中出现 “vsdbg\vs2017u5 exists, deleting”内容,可以参考下面的文章解决。

在完成调试与开发后就可以进行下一步了。

<六> 发布 DemoWeb 镜像

在完成 DemoWeb 的开发工作后,我们就可以发布该项目的镜像了。虽然你可以导出镜像到文件,但是这并不是通常的做法。镜像通常保存在容器注册表(Container Registry)(也可称其为镜像仓库)中,这样可以方便部署端随时拉取和发布端随时发布,这个镜像仓库可以是 Docker Hub,Azure 云以及各种持续交付平台。这里我以国内某云端镜像仓库服务为例演示发布的过程。

首先我们右键点击 Visual Studio 项目并点击发布,将会弹出下面的菜单,选择 【Docker 容器注册表】点击下一步。

选择发布方式

下一步的菜单让我们选择具体要发布到哪个容器注册表,这里选择【其他 Docker 容器注册表】。

下一步需要输入容器注册表地址,并输入对应的用户名和密码。

点击完成后就可以看到我们创建的的发布配置了,在这里你可以修改镜像的 tag,这里我们使用 publish 作为此次发布的镜像 tag。

完成配置后点击【发布】即可开始发布过程,首先 Visual Studio 会如下图所示重新生成项目和镜像。

完成项目和镜像的生成后,Visual Studio 就会启动 Docker push 来开始推送镜像到容器注册表中。

docker push 过程
完成发布过程

完成推送后,就可以在云端容器注册表中看到我们的镜像了。

容器注册表中的 DemoWeb 镜像

至此,发布过程完毕,这个镜像中包含了容器操作系统、.Net Core 运行时, Core 运行时以及我们的应用,仿佛一个打包了所用的内容的标准集装箱,可以部署到任何 Linux 容器引擎上了。现在你可以在任何你需要部署的 Linux 主机上运行 docker pull lvhang.tencentcloudcr.com/dev/demoweb:publish 来获取该镜像,并使用 docker run 来启动容器。

<七> 反向代理服务器 nginx 与镜像构建

如下图所示, Core 的内置 Web 服务器为 Kestrel。

Http 返回头中显示 Web Server 为 Kestrel

但在实际生产环境中为了拓展 Web 服务器的功能或者需要多个 Web 应用实例的时候,通常的做法就是添加一个反向代理服务器。

这里我们选择使用 nginx 作为反向代理服务器。nginx 是一款俄罗斯的轻量级、高性能的 http 和 反向代理 Web 服务器,被众多企业使用,更多相关的内容可以查看官网文档。

在本例中反向代理的基本原理是 nginx 接收到 Http 请求之后,再将这个请求转发到应用内的的 kestrel web 服务器,最后经由 DemoWeb 应用处理。这样如果有需要你也可以在你服务器中运行多个 DemoWeb 容器,让后将 Http 请求分别分发到这些 DemoWeb 容器并进行处理。

由于 Docker 推荐一个应用使用一个单独的容器,所以我们不应把 nginx 安装到 DemoWeb 所在的镜像中,而是应该新建一个 nginx 镜像。对于 nginx 镜像,我们要做的就是将我们配置好的配置文件拷贝到 nginx 镜像中的对应配置目录中。

首先为 nginx 准备所需的配置文件,下面这段配置应保存在 nginx 容器的 /etc/nginx/conf.d/default.conf 文件中。(这里的 server_name 在部署时需要根据实际情况设置)

这段配置的大致含义就是让 nginx 监听 80 端口,并将接受到的请求转发的 http://demoweb:80 地址。
server {
    listen        80;
    server_name   localhost;
    location / {
        proxy_pass         http://demoweb:80;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

下面就可以创建用于构建我们的 nginx 镜像的 Dockerfile 了,首先我们创建一个 nginx 文件夹,将上面的配置文件放入其中,并创建一个空的 Dockerfile 文件并写入下面的内容

#引入基础镜像
FROM nginx:1.19.2-alpine
#将当前目录的default.conf文件拷贝到镜像的/etc/nginx/conf.d目录中
COPY default.conf /etc/nginx/conf.d/default.conf

现在我们可以打开命令行工具(cmd和powershell都可以),然后进入到这个目录运行下面的命令。这里将新的 nginx 镜像命名为 my_nginx:1.19.2

docker build -t my_nginx:1.19.2 .
# -t 参数指定镜像名称和tag
# 最后的 . 指定构建目录为当前目录

镜像创建成功。

使用 docker build 命令创建自定义的 nginx 镜像

使用 docker images 命令可以查看刚创建好的镜像。

使用 docker images 命令查看新创建的镜像

现在我们就有了我们部署时所需的两个镜像了 一个 DemoWeb,一个 my_nginx。通常情况下你可能还需要一个数据库镜像,数据库镜像的构建与 my_nginx 镜像的构建类似。

下一节我们演示如何使用 docker-compose 工具将 my_nginx 和 DemoWeb 镜像“组织”在一起。

<八> 使用 docker-compose

docker-compose 是 Docker Compose 工具的命令,Compose 是一个用来定义和运行多容器 Docker 应用的工具。

它使用一个 YAML 文件来配置你应用中的各个服务,

在容器技术的上下文中一个服务对应一个容器中的程序,比如 DemoWeb 是一个服务,nginx 是一个服务,而它们作为一个整体就是一个多容器的应用。

然后只需一行命令你就可以创建和启动你配置的所有服务,而且最重要的是它为这些容器创建了一个隔离的环境。 docker-compose 通常有以下三个使用场景:

  • 开发环境,在开发软件发时,拥有可以在独立环境中运行应用程序并与之交互的能力是至关重要的。 docker-compose 工具可用于创建这样的环境并与之交互。
  • 自动化测试环境, Compose 提供了一套方便的方法来为测试套件创建和销毁隔离的测试环境。
  • 单个主机的部署,你可以使用 Compose 来部署应用到 Docker Engine,这个 Docker Engine 可以是 Dokcer Machine 的单个实例,也可以是整个 Docker Swarm 集群。

一个 Compose 中创建的所有容器存在于一个隔离的环境中,这个环境包括一个隔离的网络,其中的各个容器相当于局域网络中的每台主机,比如反向代理服务器,应用服务器,数据库服务器,缓存服务器等等,在本例中我们有两台服务器:反向代理服务器和应用服务器。更多 docker-compose 内容可查看官方文档。

了解 Comopse 后,我们需要开始规划两个容器的配置,首先 nginx 需要监听主机的 80 端口,这里我们配置 nginx 容器配置公开 80 端口并映射到主机的 80 端口,在配置文件中我们将请求转发到 http://demoweb:80

在 docker-compose 定义的这个网络中,docker-compose YAML 文件中定义的服务名称就是该容器在网络中的主机名称,所以我们可以在地址中使用主机名称。

所以我们要在 DemoWeb 容器开放 80 端口。下图显示了我们容器之间的关系以及网络端口的配置。

容器间的关系以及Http请求在容器的路径

计划好了之后,就可以在项目中添加 Compose 工具,右键点击项目选择添加【容器业务流程协调程序支持...】

添加【容器业务流程协调程序支持...】
选择 Docker Compose
选择 Linux

完成上面的步骤之后,Visual Studio 在解决方案中添加了一个 docker-compose 项目。

添加完 docker-compose 支持后的解决方案

以下是默认的 docker-compose 文件内容

version: '3.4'

services:
  demoweb:
    image: ${DOCKER_REGISTRY-}demoweb
    build:

      context: .
      dockerfile: DemoWeb/Dockerfile

根据我们的规划将其改为以下内容,

version: '3.4'

services:
  demoweb:
    container_name: DemoWeb
    image: demoweb:dev
    build:
      context: .
      dockerfile: DemoWeb/Dockerfile
  nginx:
    container_name: my_nginx
    image: my_nginx:1.19.2
    depends_on: 
        - demoweb
    ports:
      - 80:80

此时点击 docker-compse 便可开始生成与调试,成功之后便可以使用 http://localhost 地址访问网站.

使用80端口访问 DemoWeb

下一节介绍使用 docker-compose 在服务器端部署我们的应用。

<九> 服务器端部署

因为 DemoWeb 镜像已经上传到云端镜像仓库,nginx 也是 Docker Hub 中的镜像,所以我们只需要将 docker-compose.yml 文件以及 nginx 配置文件上传到服务器即可。当然服务器必须安装 Docker 以及 Compose 工具。

由于我们需要在“部署现场”构建 my_nginx 镜像,并且我们需要修改 DemoWeb 镜像为镜像仓库地址,所以如下修改了 docker-compose 文件,简单说就是 DemoWeb 镜像在线拉取,my_nginx 镜像现场构建。

version: '3.4'

services:
  demoweb:
    container_name: DemoWeb
    image: lvhang.tencentcloudcr.com/dev/demoweb:publish
  nginx:
    container_name: my_nginx
    image: my_nginx:1.19.2
    build:
      context: ./nginx
      dockerfile: Dockerfile
    depends_on: 
        - demoweb
    ports:
      - 80:80

最后我们新建一个 my_app 文件夹,将新的 docker-compose.yml 文件拷贝其中,并将之前用于构建 my_nginx 镜像的目录 nginx 也拷贝其中。整体目录结构如下

my_app
my_app/docker-compose.yml
my_app/nginx
my_app/nginx/default.conf
my_app/nginx/Dockerfile

最后将该目录拷贝到服务器,在服务器端进入部署文件夹目录,并运行启动命令 docker-compose up。

ubuntu@VM-0-4-ubuntu:~/my_app$ docker-compose up
Creating network "my_app_default" with the default driver
Pulling demoweb (lvhang.tencentcloudcr.com/dev/demoweb:publish)...
publish: Pulling from dev/demoweb
bf5952930446: Pull complete
95f9f5484a21: Pull complete
ebc43d54b0d9: Pull complete
eb8b3fc30ae1: Pull complete
c42d79623507: Pull complete
5d3ef46ee75e: Pull complete
Digest: sha256:92f45ef82db3c2f3832a81b5caa1adda469d2566a7e1613142faeb367961e2c3
Status: Downloaded newer image for lvhang.tencentcloudcr.com/dev/demoweb:publish
Building nginx
Step 1/3 : FROM nginx:1.19.2-alpine
1.19.2-alpine: Pulling from library/nginx
df20fa9351a1: Pull complete
3db268b1fe8f: Pull complete
f682f0660e7a: Pull complete
7eb0e8838bc0: Pull complete
e8bf1226cc17: Pull complete
Digest: sha256:a97eb9ecc708c8aa715ccfb5e9338f5456e4b65575daf304f108301f3b497314
Status: Downloaded newer image for nginx:1.19.2-alpine
 ---> 6f715d38cfe0
Step 2/3 : EXPOSE 80
 ---> Running in b7f7aa15f977
Removing intermediate container b7f7aa15f977
 ---> fe843fbc2e0c
Step 3/3 : COPY default.conf /etc/nginx/conf.d/default.conf
 ---> c9983b01a0a8

Successfully built c9983b01a0a8
Successfully tagged my_nginx:1.19.2
WARNING: Image for service nginx was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating DemoWeb ... done
Creating my_nginx ... done
Attaching to DemoWeb, my_nginx
DemoWeb    | info: Microsoft.Hosting.Lifetime[0]
DemoWeb    |       Now listening on: http://[::]:80
DemoWeb    | info: Microsoft.Hosting.Lifetime[0]
DemoWeb    |       Application started. Press Ctrl+C to shut down.
DemoWeb    | info: Microsoft.Hosting.Lifetime[0]
DemoWeb    |       Hosting environment: Production
DemoWeb    | info: Microsoft.Hosting.Lifetime[0]
DemoWeb    |       Content root path: /app
my_nginx   | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
my_nginx   | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
my_nginx   | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
my_nginx   | 10-listen-on-ipv6-by-default.sh: Getting the checksum of /etc/nginx/conf.d/default.conf
my_nginx   | 10-listen-on-ipv6-by-default.sh: error: /etc/nginx/conf.d/default.conf differs from the packages version
my_nginx   | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
my_nginx   | /docker-entrypoint.sh: Configuration complete; ready for start up

命令运行成功完成,便可访问网站进行测试。

部署成功

<十> 结语

至此,本文从 Visual Studio 单容器调试、镜像的发布、docker-compose 的多容器调试到最后的 docker-compose 多容器应用部署的全程已结束,虽然整个过程相对传统的发布显得比较复杂,但是 docker 为从开发到部署过程的自动化和标准化提供了可能,个人相信这也是软件开发的的发展方向。

本文内容较多,而且很多细节没有详细说明白,所以如果有关于本文的任何问题或有任何指正请在评论区中提出,我会尽量回复和修正。

感谢阅读,希望本文能对你有所帮助。

<十一> 参考资料

Dockerfile 参考

Docker CLI

Docker Compose 参考

nginx 文档

微软 Docker 文档

linux 安装 Docker

linux 安装 Docker Compose

 

转 https://zhuanlan.zhihu.com/p/222977899?utm_source=ZHShareTargetIDMore

posted @   dreamw  阅读(991)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2022-03-09 [学习笔记] 哈希函数和 SHA-256
2021-03-09 Vue CLI配置原理详解
点击右上角即可分享
微信分享提示