Docker是一系列创建和管理容器的工具,它能够将服务器上的应用程序相互隔离起来。这一系列文章是我学习在ASP.NET Core MVC中使用Docker实践的学习笔记,参考《Essential Docker for ASP.NET Core MVC》这本书。

Docker能解决什么问题?


Docker能解决ASP.NET Core MVC项目中存在的两个复杂性问题:一致性问题和响应性问题。

一致性问题

    大部分的ASP.NET Core MVC应用都由多个组件组成,比如至少需要一个服务器来运行MVC应用,以及一个数据库用来持久化数据。

    一些复杂的应用可能还需要其他的一些组件,比如多应用服务器来共享工作量,负载均衡用来在多个服务器之间分发HTTP请求、数据缓存组件用来提供性能。随着组件的增加,额外的服务器可能会被加入进来,这就需要网络将这些组件连接起来,命名服务器用来提供服务发现以及存储阵列用来提供数据恢复等等。

    很少有项目能够给每一个开发者提供和实际生产环境中一样的开发环境。这就导致,开发人员是在跟生产环境相似的系统下开发,特别的这些所有的组件都运行在开发者的单个开发环境上,而忽略了一些典型的基础设施,比如网络和负载均衡。

    因为开发环境跟实际的生产环境不一致可能会导致一些问题。

    第一个问题是开发环境和生产环境的不同会导致应用程序部署后会产生一些不可预料的行为。比如,项目在Windows上开发,但是被部署到了Linux服务器上,但是这两者的文件系统,存储位置以及其他很多特性都有不同。

    第二个问题是开发人员之间用以表示生产环境的开发环境可能差异巨大。不同开发人员之间,开发工具的版本及其依赖可能不同、NuGet包、甚至.NET Core和ASP.NET Core运行时都有可能不同。这使得某个开发人员写的代码在生产环境,或者其他开发人员机器上运行不起来,因为每个人都有自己认为跟生产环境相似的开发环境。

    第三个问题是实际部署的问题,一般开发和部署需要两套配置文件,这两套配置文件不容易测试,直到部署之后才能测试。

    第四个问题是很难保证应用程序在所有的服务器上都配置一致。错误的配置可能会产生一些奇怪的问题,比如当用户的HTTP请求被分发到很多服务器上,要找到错误的那台并隔离出来很困难。

Docker如何解决一致性问题

    将ASP.NET Core MVC应用程序放到容器里,这个过程就被称为容器化(Containerization)。创建一个镜像(Image),镜像是容器(Container)的模板,他包含应用程序所有的环境。所有被用来运行应用程序的东西都是镜像的一部分,比如.NET Core运行时,ASP.NET Core包,第三方工具,配置文件,用户自定义的类,Razor视图等等。

    Docker使用镜像来创建容器,所有从相同镜像创建出来的容器都包含了相同的ASP.NET Core MVC应用程序。

    如果在开发环境就在项目中使用Docker,所有的开发人员就能使用单个镜像来创建和测试应用程序。虽然开发环境的镜像跟实际生成环境的系统仍然可能有差异,但这种更容易复制,并且这种差别只是开发工具,比如编译器和调试器的差异。不论如何,开发阶段的镜像包含了跟部署环境同样的内容,他们包含了同样的文件系统,同样的网络拓扑,同样的NuGet包以及同样的.NET 运行时。

    当应用程序准备部署的时候,可以创建生产环境的镜像。这种镜像跟开发环境镜像相似,只是去掉了一些开发工具以及包含了编译后的C#的类。生产环境的镜像用来创建各自生产环境的实例,他能够保证所有的实例配置都是相同的。既然开发环境和生产环境的镜像包含了相同的内容,那就没必要在生产环境修改配置文件了,因为在开发阶段的数据库连接字段串能工作,那么在生产环境同样可以。

响应性问题

    传统的ASP.NET Core MVC的部署方式很难对工作负载的变化做出响应。在Windows Server上,部署到IIS服务器上应用程序,想要增加服务容量是一项艰巨的任务,他需要额外的硬件,以及配置来将新的服务器加入到环境中。

    当流量激增时在短期内对系统进行扩容,以及流量峰值过后对系统进行缩容,这种操作使用传统的部署方式很难实现。这种导致ASP.NET应用程序的服务能力在跟实际的工作负载的匹配上挣扎,要么就是在流量高峰时处理能力不够(影响用户体验),要么就在流量低谷时处理能力过剩(浪费金钱),不能按需灵活扩容缩容。

Docker如何解决响应性问题

     容器是对应用程序的一种轻量化包装,他能够在保证隔离性的前提下提供足够的资源来运行应用程序。单个服务器能够运行多个容器,Docker提供了一种叫“Swarm”的内在集群机制,使得能够不需要对集群配置进行修改就能够部署容器到集群中去。结合低资源需求以及内在的集群特性,使得对容器化的ASP.NET Core MVC应用程序进行扩容或者缩容的操作变成了添加和移除容器的过程。另外,因为容器间是彼此隔离的,任何没有用到的容器能够被用来运行其他应用的容器,这就使得应用能够根据负载动态调整和平衡。

Docker容器和虚拟机的区别


    初看起来,容器跟虚拟机很像,虽然这两者工作方式不同,但确实有相似的地方。他们都能通过添加和删除实例来扩展应用程序,并且能够用来为应用程序创建标准化的环境。

    但容器不是虚拟机。虚拟机提供了一种完全的隔离,虚拟机内包括操作系统。比如单个服务器,能够运行多个虚拟机,每个虚拟机能够安装不同的操作系统,不同的应用程序能够同时安装在Linux和Windows的虚拟机上。

    而Docker只提供了应用程序方面的隔离,单个服务器上的所有容器都运行在这个服务器的操作系统上,这就意味着,在Linux服务器上只能运行Linux容器,在Windows服务器上能运行Windows容器,当然在Windows 10上,通过windows subsystem for linux技术,能够在Windows上运行Linux容器。

    因为Docker容器只是对应用程序的隔离,所以相比虚拟机,他所需要的资源要少得多,因此一台服务器能够运行比虚拟机多得多的容器。这并不是说服务器运行容器能够提供更多的处理能力,但他意味着需要更少的资源来处理一些低级别的操作系统任务,而这些在每个虚拟机上都是重复的。

▲ Docker跟虚拟机的不同

     上图演示了Docker跟虚拟机的典型区别,对于ASP.NET Core MVC应用程序来说,Docker的一些特性使得我们能够很方便的创建容器的副本,而不需要修改配置,使得我们能够根据HTTP请求量来自动扩充实例,这对于解决响应性和一致性问题提供了一种优雅的方案,而这种需求使用传统的虚拟机是很难满足的。

Docker环境安装


    我这里使用的Windows 10 操作系统,要在Windows上使用Docker,需要去官网下载Docker安装包,下载完成之后,直接安装即可。如果是安装的比较新的Windows 10版本,则支持WSL2,据说可以提高性能。

    Docker安装完成之后,需要重启操作系统,然后可能还要安装Windows Subsystem for Linux补丁。安装完成之后,为了验证Docker环境是否正确安装,在PowerShell或者Docker中,输入一下命令,下载测试实例:

PS C:\Users\yangyang> docker run --rm hello-world

    输出结果如下,则表示Docker安装成功:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

    docker run表示运行某容器,首先查找本地是否存在该容器,如果不存在,则下载。--rm 表示当程序停止时,移除该容器。

    如果打开 Docker Desktop,在Images里可以看到刚下载的hello-world镜像。

ASP.NET Core MVC开发环境


    为了达到装逼和唬人的效果,这里选择Visual Studio Code,而不是号称宇宙第一IDE的Visual Studio 2019。首先去官网下载Visual Studio Code for Windows的安装包,然后安装C#和Dock插件,在Visual Studio Code中编写C#可以参考官方教程

▲ 在Visual Studio Code中安装插件

    安装完成之后,我们创建第一个MVC应用程序,注意,这里创建MVC初始程序模板跟我们在Visual Studio 2019中能够使用鼠标点点就出来不一样,我们需要首先在想要创建项目的文件夹下,新建名为项目名的文件夹,比如这里,我在“D:\Study\DockStudyInVSCode”文件夹下,新建了一个ExampleApp文件夹。然后,使用cmd命令行工具,切换到ExampleApp文件夹下,新建一个ASP.NET Core MVC项目:

dotnet new mvc --language C#

   然后,打开Visual Studio Code,在File->Open Folder,选择之前的文件夹,“D:\Study\DockStudyInVSCode”,他就能把这个文件夹下所有的包含项目文件的项目都加进来。

▲ Visual Studio Code中,打开项目的方式,要选项目所在的文件夹

    打开完成之后,可以看到一个基本的ASP.NET Core MVC程序就有了,接下来,选择项目文件,右键,打开到Terminal中,其实就是把PowerShell嵌入到了Visual Studio Code中,并且已经切换到了该项目文件下,然后运行 dotnet restore、dotnet run,就可以把程序运行起来。

▲切换到Power Shell控制台,并切换到当前目录下 

    可以看到,在本地,如果打开浏览器,输入 https://localhost:5001/ 就可以看到程序正常运行起来了。

    目前,程序过于简单,且没有运行在Docker中,接下来对程序进行一些修改。

项目基础


    首先添加一些Model,用来承载和产生实验用的数据,在Visual Studio Code中,选择Model文件夹,然后右键NewFile,名字命名为Product.cs,然后手动输入以下内容。

namespace ExampleApp.Models {
    public class Product {
        public Product() {}
        public Product(string name = null,string category = null,decimal price = 0) 
        {
            Name = name;
            Category = category;
            Price = price;
        }
        public int ProductID { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

    随后在该文件夹下,新建IRepository.cs接口,用来依赖注入具体实现,内容如下:

using System.Linq;

namespace ExampleApp.Models
{
    public interface IRepository
    {
        IQueryable<Product> Products { get; }
    }
}

    最后在该文件夹下,新建一个DummyRepository, 实现IRepository接口,用来产生实验数据,内容如下:

using System.Linq;
namespace ExampleApp.Models
{
    public class DummyRepository : IRepository
    {
        private static Product[] DummyData = new Product[]{
            new Product{Name="Prod1",Category="Cat1",Price=100},
            new Product{Name="Prod2",Category="Cat1",Price=100},
            new Product{Name="Prod3",Category="Cat2",Price=100},
            new Product{Name="Prod4",Category="Cat2",Price=100}
        };
        public IQueryable<Product> Products => DummyData.AsQueryable();
    }
}

    然后,修改HomeController.cs,在Index返回的内容中输出Product数据,内容如下:

using Microsoft.AspNetCore.Mvc;
using ExampleApp.Models;
using Microsoft.Extensions.Configuration;

namespace ExampleApp.Controllers
{
    public class HomeController : Controller
    {
        private IRepository repository;
        private string message;
        public HomeController(IRepository repo, IConfiguration config)
        {
            repository = repo;
            message = config["MESSAGE"] ?? "Essential Docker";
        }

        public IActionResult Index()
        {
            ViewBag.Message = message;
            return View(repository.Products);
        }
    }
}

    最后,修改Views/Home/Index.cshtml,内容如下:

@model IEnumerable<Product>
@{
    Layout = null;
}
<!DOCTYPE html>
<html>

<head>
    <meta name="viewport" content="width=device-width" />
    <title>ExampleApp</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
</head>

<body>
    <div class="m-1 p-1">
        <h4 class="bg-primary text-center p-1 text-white">
            @ViewBag.Message
        </h4>
        <table class="table table-sm table-striped">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>Name</th>
                    <th>Category</th>
                    <th>Price</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var item in Model)
                {
                    <tr>
                        <td>@item.ProductID</td>
                        <td>@item.Name</td>
                        <td>@item.Category</td>
                        <td>@item.Price.ToString("F2")</td>
                    </tr>
                }
            </tbody>
        </table>
    </div>
</body>
</html>

    在Windows上的Visual Studio Code中,格式化代码的命令为“Shift+Alt+F"(Mac上为“Shift + Option + F”,Unbuntu上为“Ctrl + Shift + I”)。

    最后,输入dotnet run,在浏览器中,可以看到,页面变为:

    目前,我们的ASP.NET Core MVC应用程序并没有运行在Docker中,接下来将其部署到Docker中。

Docker初步


Docker镜像

    Docker镜像(image)是生成勇气的模板,它包含了创建容器所需要的所有文件。在安装Docker判断是否安装成功时,我们下载了一个名为hello-world的镜像。这个镜像是Docker Hub里面发布的一个公共的镜像。hello-world镜像包含了一个最基本的打印一条hello world信息的应用程序所需要的所有文件。启动docker,在powershell中,我们可以通过“docker images”命令打印所有的安装的镜像:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY          TAG       IMAGE ID       CREATED        SIZE
hello-world         latest    d1165f221234   5 weeks ago    13.3kB

    可以看到,改名了列出了所有镜像,比较重要的是“IMAGE ID” 这一项目,它用来唯一标识一个镜像,目前只有1个镜像。

    在Docker Desktop中,也可以看到当前机器上的所有镜像,如下图:

下载Docker镜像

    可以通过docker pull命令来从Docker Hub上下载公共的镜像到本地。比如我们下载一个名为 “alpine”的镜像到本地,命令为“docker pull alpine”,下载完成之后,再次运行“docker images”,可以看到多了一个镜像:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker pull alpine 
Using default tag: latest
latest: Pulling from library/alpine
540db60ca938: Pull complete
Digest: sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f
Status: Downloaded newer image for alpine:latest
docker.io/library/alpine:latest

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY          TAG       IMAGE ID       CREATED        SIZE
alpine                  latest    6dbb9cc54074   12 hours ago   5.61MB
hello-world         latest    d1165f221234   5 weeks ago    13.3kB

     可以看到,在Docker Desktop里面,也有:

    在下载镜像时,也可以指定tag标签,也就是版本,如果不指定,就默认是最新的版本(latest),这里使用"docker pull alpine:3.4" 下载一个3.4版本的alpine。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker pull alpine:3.4
3.4: Pulling from library/alpine
c1e54eec4b57: Pull complete
Digest: sha256:b733d4a32c4da6a00a84df2ca32791bb03df95400243648d8c539e7b4cce329c
Status: Downloaded newer image for alpine:3.4
docker.io/library/alpine:3.4
 
PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY          TAG       IMAGE ID       CREATED        SIZE
alpine              latest    6dbb9cc54074   12 hours ago   5.61MB
hello-world     latest    d1165f221234   5 weeks ago    13.3kB
alpine              3.4       b7c5ffe56db7   2 years ago    4.82MB

    下载完成之后,再次运行“docker images”,可以看到,我们有两个alpine镜像,其中一个tag是最新的,一个是3.4版本,两者 IMAGE ID不一样。

删除Docker镜像

     可以通过"docker rmi" 来删除一个或者多个镜像,如果要删除某一个镜像,则只需要在 docker rmi -f IMAGE ID,即可,比如我们删除alpine tag为3.4的镜像,该镜像IMAGE ID 为 b7c5ffe56db7

PS D:\Study\DockStudyInVSCode\ExampleApp> docker rmi -f b7c5ffe56db7
Untagged: alpine:3.4
Untagged: alpine@sha256:b733d4a32c4da6a00a84df2ca32791bb03df95400243648d8c539e7b4cce329c
Deleted: sha256:b7c5ffe56db790f91296bcebc5158280933712ee2fc8e6dc7d6c96dbb1632431
Deleted: sha256:23f7bd114e4a0ea39a34e1bd50c2b71fb7d60f89139ca86ee746efb0bb49b4b8
PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY          TAG       IMAGE ID       CREATED        SIZE
alpine              latest    6dbb9cc54074   12 hours ago   5.61MB
hello-world     latest    d1165f221234   5 weeks ago    13.3kB

    可以看到,删除成功。

    如果要删除所有镜像,可以使用 docker rmi -f  $(docker images -q) ,docker images -q 参数表示quite,仅返回IMAGE ID,这些IMAGE ID 正好被docker rmi -f 接受。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images -q  
6dbb9cc54074
d1165f221234

PS D:\Study\DockStudyInVSCode\ExampleApp> docker rmi -f  $(docker images -q)
Untagged: alpine:latest
Untagged: alpine@sha256:69e70a79f2d41ab5d637de98c1e0b055206ba40a8145e7bddb55ccc04e13cf8f
Deleted: sha256:6dbb9cc54074106d46d4ccb330f2a40a682d49dda5f4844962b7dce9fe44aaec
Deleted: sha256:b2d5eeeaba3a22b9b8aa97261957974a6bd65274ebd43e1d81d0a7b8b752b116
Untagged: hello-world:latest
Untagged: hello-world@sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Deleted: sha256:d1165f2212346b2bab48cb01c1e39ee8ad1be46b87873d9ca7a4e434980a7726
Deleted: sha256:f22b99068db93900abe17f7f5e09ec775c2826ecfe9db961fea68293744144bd

自定义镜像


    了解了镜像的基本操作之后,现在我们需要自定义镜像,将我们的ASP.NET Core MVC应用程序打包到镜像中。自定义镜像一般是在Docker文件中定义,一般命名为Dockerfile,我们在ExampleApp项目下,新建文件Dockerfile(安装Docker插件之后,快捷键Ctrl+Shift+P,在弹出的选项中,选择Docker: Add Docker Files to Workspace,然后一步一步能够自动创建Docker文件,详见使用说明),内容如下:

FROM mcr.microsoft.com/dotnet/aspnet:5.0
COPY dist /app
WORKDIR /app
EXPOSE 80/tcp
ENTRYPOINT ["dotnet", "ExampleApp.dll"]

第一句:“FROM mcr.microsoft.com/dotnet/aspnet:5.0”,FORM开头用来设置基础镜像。Docker镜像的一个最强大功能是,他能够将已经存在镜像作为基础镜像使用,在本例中,我们将设置的基础镜像为aspnet版本为5.0。这个镜像由微软提供,它包含了.NET Core运行时和ASP.NET Core需要的包,并且已经编译成了本地代码以提高启动性能。这个镜像不包含.NET SDK,所以在使用之前,我们需要把应用程序编译并发布好,这部分操作在后面进行。

第二句:“COPY dist /app”,这一句表示将 dist 文件夹下的所有内容复制到容器里一个叫app的文件夹下,现在dist文件夹还不存在,他主要存放编译后的准备发布用的ASP.NET Core MVC应用程序.

第三句:“WORKDIR /app”,这一句表示将当前容器里的/app目录设置为工作目录。这样就不用在运行命令时,每次都切换到这个目录下了。

第四句:“EXPOSE 80/tcp”,容器里的进程能够访问网络端口而不需要任何操作,但是Docker不允许外部的应用程序来访问docker里面容器的端口,除非在Dockerfile里通过EXPOSE关键字开放和制定了端口。“80/tcp”告诉Docker,在容器外面可以通过tcp的80端口访问容器内部。这使得容器内部的ASP.NET Core服务器能够接收HTTP请求。

第五句:“ENTRYPOINT ["dotnet", "ExampleApp.dll"]”,最后一句使用了ENTRYPOINT命令,告诉当容器开始运行时需要进行的操作。这里这个语句告诉Docker运行dotnet语句,并且执行ExampleApp.dll文件,这个文件在前面指定的WORKDIR即Docker容器里的/app文件夹内。

    或者使用自动生成的模板,里面内容如下:

#使用asp.net 5.0作为基础镜像,起一个别名为base
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
#设置容器的工作目录为/app
WORKDIR /app
#暴露容器的tcp 80端口
EXPOSE 80/tcp

ENV ASPNETCORE_URLS=http://+:80

# Creates a non-root user with an explicit UID and adds permission to access the /app folder
# For more info, please refer to https://aka.ms/vscode-docker-dotnet-configure-containers
RUN adduser -u 5678 --disabled-password --gecos "" appuser && chown -R appuser /app
USER appuser

#使用.net sdk 5.0作为基础镜像,起一个别名为build
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
#设置容器的工作目录为/src
WORKDIR /src
#将当前本机ExampleApp/ExampleApp.csproj项目文件复制到容器中的/src/ExampleApp/目录下
COPY ["ExampleApp/ExampleApp.csproj", "ExampleApp/"]
#执行dotnet restore,还原本地文件
RUN dotnet restore "ExampleApp/ExampleApp.csproj"
#复制当前目录文件,到容器的/src目录下
COPY . .
#设置容器的工作目录为/src/ExampleApp
WORKDIR "/src/ExampleApp"
#执行编译生成,以Release模式生成到容器的/app/build目录下
RUN dotnet build "ExampleApp.csproj" -c Release -o /app/build

#以上面的build作为基础镜像,重命名为publish
FROM build AS publish
#执行dotnet publish命令,发布到容器的/app/publish目录下
RUN dotnet publish "ExampleApp.csproj" -c Release -o /app/publish

#将上面的base作为基础镜像,重命名为final
FROM base AS final
#设置容器的工作目录为/app
WORKDIR /app
#拷贝/app/publish到当前工作目录
COPY --from=publish /app/publish .
#指定容器入口命令,容器启动时,会运行dotnet ExampleApp.dll
ENTRYPOINT ["dotnet", "ExampleApp.dll"]

    如果以以上作为模板,则生成自定义镜像的时候,需要注意要切换到根目录。如下:

PS D:\Study\DockStudyInVSCode> docker build . -t dockerstudy/exampleapp -f ExampleApp/Dockerfile

    在Visual Studio Code中开发ASP.NET Core程序并部署到容器中的更多信息,可以查看  ASP.NET Core in a container

准备好应用程序


   前面的Dockerfile中,并没有包含基本的.NET SDK用来编译和发布我们的ASP.NET Core应用程序,所以我们需要使用以下命令来编译和发布应用程序。

PS D:\Study\DockStudyInVSCode\ExampleApp> dotnet restore
  Determining projects to restore...
  All projects are up-to-date for restore.

PS D:\Study\DockStudyInVSCode\ExampleApp> dotnet publish --framework net5.0 --configuration Release --output dist
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  ExampleApp -> D:\Study\DockStudyInVSCode\ExampleApp\bin\Release\net5.0\ExampleApp.dll
  ExampleApp -> D:\Study\DockStudyInVSCode\ExampleApp\bin\Release\net5.0\ExampleApp.Views.dll
  ExampleApp -> D:\Study\DockStudyInVSCode\ExampleApp\dist\

    “dotnet restore” 命令用来恢复应用程序包,“dotnet publish”用来发布应用程序到“dist”文件夹内。

 创建自定义镜像


    现在应用程序已经发布到了dist文件夹内,并且Dockerfile已经写好,接下来就可以创建自定义的应用程序了。将powershell切换到ExampleApp文件夹下,然后运行下面的命令:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker build . -t dockerstudy/exampleapp -f Dockerfile
[+] Building 8.4s (8/8) FINISHED
 => [internal] load build definition from Dockerfile                                                                                                                                                                1.2s 
 => => transferring dockerfile: 169B                                                                                                                                                                                0.1s 
 => [internal] load .dockerignore                                                                                                                                                                                   1.4s 
 => => transferring context: 2B                                                                                                                                                                                     0.0s 
 => [internal] load metadata for mcr.microsoft.com/dotnet/aspnet:5.0                                                                                                                                                0.9s 
 => [internal] load build context                                                                                                                                                                                   1.2s 
 => => transferring context: 4.67MB                                                                                                                                                                                 0.5s 
 => CACHED [1/3] FROM mcr.microsoft.com/dotnet/aspnet:5.0@sha256:c0cc95b0d87a31401763f8c7b2a25aa106e7b45bfcaa2f302dc9d0ff5ab93fa2                                                                                   0.0s 
 => [2/3] COPY dist /app                                                                                                                                                                                            1.4s 
 => [3/3] WORKDIR /app                                                                                                                                                                                              1.1s 
 => exporting to image                                                                                                                                                                                              1.7s 
 => => exporting layers                                                                                                                                                                                             1.3s 
 => => writing image sha256:0c0b241677976825d4b6d40426f85b5c9207867b1829427ccf28d5ae15b215fa                                                                                                                        0.0s 
 => => naming to docker.io/dockerstudy/exampleapp 

    使用“docker build”方法用来创建镜像,build后面的 "."表示当前文件夹,他是docker里面执行COPY的文件夹。“-t” 参数指定了镜像的名称。“-f”参数指定用来创建镜像的Dockerfile文件。执行完上述命令,没报错的话,可以再执行“docker images”可以看到刚才创建的镜像:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY               TAG       IMAGE ID       CREATED         SIZE
dockerstudy/exampleapp   latest    0c0b24167797   3 minutes ago   210MB

使用容器


    有了镜像之后,就能根据镜像创建容器了。通过"docker create"命令能够创建容器:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker create -p 3000:80 --name exampleApp3000 dockerstudy/exampleapp
f6c44d99b13efd48dc274b63bef992a17741bddb19f3c0a601167947d3279313

    “docker create”可以创建容器,参数“-p”用来告诉Docker将容器里的80端口,映射到宿主机器的3000端口上。这个80端口,也是在Dockerfile文件里通过EXPOSE定义的。参数“--name” 用来给容器命名。使得我们后续方便对新创建的容器进行操作。这里命名为了example3000,告诉宿主机器,需要访问3000端口。最后一个参数告诉Docker使用那个镜像来创建容器,这里使用的“dockerstudy/exampleapp”这个镜像,这个镜像是上面我们通过“docker build”来创建的。我们接下使用相同的方法再创建一个容器。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker create -p 4000:80 --name exampleApp4000 dockerstudy/exampleapp
b4e078e0f7dd0a5c6fdb721e4b232d98e778132b0db411135395f8888305896c

    现在我们创建了一个新的容器“exampleApp4000”,他将本机的4000端口映射到容器的80端口上。这两个容器使用的是相同的镜像创建的,所以里面的内容完全相同。

    容器创建好了之后,我们可以通过“docker ps -a”来列出所有容器。 “docker ps”命令会忽略没有在运行的容器,所以需要加参数“-a”来列出所有本机上的容器。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS    PORTS     NAMES
b4e078e0f7dd   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   2 minutes ago    Created             exampleApp4000
f6c44d99b13e   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   13 minutes ago   Created             exampleApp3000

    可以看到,这两个容器的IMAGE是一样的,并且状态都是Created,表示容器已经成功创建,等待运行。PORTS列目前为空,表示当前没有容器激活网络端口。在后续会变更。

    在Docker Desktop里面的容器列表里,也可以看到目前docker里面的所有容器。

    使用“docker start” 就可以将容器运行起来。通过添加 CONTAINER ID或者NAMES参数,可以运行一个或多个容器,这里我们先运行example4000这个容器。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker start exampleApp4000
exampleApp4000

    现在exampleApp4000这个容器已经运行起来了,再次执行“docker ps -a”可以看到Status和Ports栏有变化:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS              PORTS                  NAMES
b4e078e0f7dd   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   9 minutes ago    Up About a minute   0.0.0.0:4000->80/tcp   exampleApp4000
f6c44d99b13e   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   20 minutes ago   Created                                    exampleApp3000

    可以看到exampleApp4000容器已经运行起来了,端口是本机的4000端口映射到了容器里的80端口。在Docker Desktop里也可以看到状态更新:

    现在,打开浏览器,输入 http://localhost:4000/ 就能看到如下界面。

    这个ASP.NET Core程序目前是允许在Docker容器里了。

    还可以通过“docker start $(docker ps -aq)”来运行所有容器。"docker ps"是列出容器的命令,-a参数表示所有容器,包括不在允许的,-q表示quite,只返回容器ID。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker ps -aq
b4e078e0f7dd
f6c44d99b13e

     运行“docker start $(docker ps -aq)”:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker start $(docker ps -aq)
b4e078e0f7dd
f6c44d99b13e

    现在再次运行" docker ps -a",可以看到信息更新了。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS          PORTS                  NAMES
b4e078e0f7dd   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   20 minutes ago   Up 13 minutes   0.0.0.0:4000->80/tcp   exampleApp4000
f6c44d99b13e   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   31 minutes ago   Up 47 seconds   0.0.0.0:3000->80/tcp   exampleApp3000

    现在整个从ASP.NET Core MVC应用程序到镜像再到容器的结构为:

停止容器

    通过"docker stop" 语句可以停止容器。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker stop exampleApp3000
exampleApp3000

   通过“docker stop $(docker ps -q)”,可以停止所有容器。这里docker ps 没有包含 -a,表示只返回处在运行中的容器ID,没有运行的因为不需要停止,所以这里不需要返回。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker stop $(docker ps -q)
b4e078e0f7dd

    上面exampleApp3000已经停止了,运行上面命令后,只需要停止exampleApp4000这个容器。

     再次查看容器状态,可以看到两个容器的状态都变成了Exit:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker ps -a
CONTAINER ID   IMAGE                    COMMAND                  CREATED          STATUS                          PORTS     NAMES
b4e078e0f7dd   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   27 minutes ago   Exited (0) About a minute ago             exampleApp4000
f6c44d99b13e   dockerstudy/exampleapp   "dotnet ExampleApp.d…"   38 minutes ago   Exited (0) 3 minutes ago                  exampleApp3000

修改容器内容


   我们先把两个容器,通过“docker start $(docker ps -aq)” 都运行起来,然后打开浏览器,可以看到两者内容一模一样。

   现在我们准备修改,其中一个容器里的内容。首先编辑/Views/Home/Index.cshtml文件,内容修改如下:

 ......
<h4 class="bg-success text-center p-1 text-white">
    This is new Content
</h4>
 ......

   我们修改了H4的样式和内容。由于ASP.NET Core MVC应用程序编译之后,视图都编译到了ExampleApp.View.dll中,所以上述修改只会,我们需要使用命令重新发布来重新生成ExampleApp.View.dll,我们重新执行dotnet publish命令:

PS D:\Study\DockStudyInVSCode\ExampleApp> dotnet publish --framework net5.0 --configuration Release --output dist
Microsoft (R) Build Engine version 16.8.0+126527ff1 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  ExampleApp -> D:\Study\DockStudyInVSCode\ExampleApp\bin\Release\net5.0\ExampleApp.dll
  ExampleApp -> D:\Study\DockStudyInVSCode\ExampleApp\bin\Release\net5.0\ExampleApp.Views.dll
  ExampleApp -> D:\Study\DockStudyInVSCode\ExampleApp\dist\

    现在在dist文件夹下,我们得到了新的修改后的ExampleApp.View.dll文件,现在需要把改文件上传到ExampleApp3000这个容器的文件系统中。首先我们来查看一下ExampleApp3000这个容器里面存在的文件系统,可以在Visual Studio Code的Docker插件中查看,比如如下图:

同样,在Docker Desktop中,通过打开命令行程序,也可以使用linux语句来显示所有文件系统,比如:

在弹出的窗体中,输入ls,列出当前容器下的所有文件。

    还可以直接打开容器的交互界面。比如使用如下命令:

//-i参数表示确保标准输入流保持开放,需要在shell中输入命令,
//-t参数表示分配一个伪终端(TTY)
//-it 也可以理解为interactive
docker exec -it exampleApp3000 /bin/bash  

    还可以直接编辑配置文件:

docker exec exampleApp3000 cat /app/web.config

    现在,我们需要通过命令,将本机dist文件夹下的ExampleApp.View.dll,拷贝到ExampleApp3000的容器内。语句为:

docker cp ./dist/ExampleApp.Views.dll exampleApp3000:/app

    完成之后,执行命令,重启exampleApp3000容器:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker restart exampleApp3000
exampleApp3000

    现在刷新浏览器,可以看到localhost:3000页面内容发生了改变,而localhost:4000内容没变,这表明两个容器是完全独立的:

基于修改后的容器创建新镜像


基于上述修改后的容器,我们可以创建一个新的镜像,使用docker commit可以完成这一任务:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker commit exampleApp3000 dockerstudy/exampleapp:changed
sha256:4e4417cec9c1eb5fd247eae9124885f6ed256a46358fcdf494271dd97a5aee9a

    现在执行docker images可以看到新增加了一个镜像,只是TAG变成了我们指定的changed:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY                         TAG       IMAGE ID       CREATED              SIZE
dockerstudy/exampleapp   changed   4e4417cec9c1   About a minute ago   210MB
dockerstudy/exampleapp   latest        6a3640ffcaf0    17 hours ago         210MB

将镜像发步到Docker Hub


    本地创建的自定义镜像可以发不到Docker Hub上,这类似于将代码发不到Github。在推送之前必须要在Docker Hub上注册一个账户。需要注意的是,在发布到Docker Hub之前,需要修改镜像的tag,在本地我有两个镜像,分别是“dockerstudy/exampleapp:changed”和“dockerstudy/exampleapp:latest”,这两个镜像的前缀是“dockerstudy”,发不到Docker Hub的前缀,必须要修改成在Docker Hub上注册的用户名,我这里注册的用户名为xjdx2008,所以我需要把这两个镜像的前缀充dockerstudy改为xjdx2008。

    可以通过docker tag语句,来基于已有的镜像创建新的镜像给予不同的镜像名称:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker tag dockerstudy/exampleapp:changed xjdx2008/exampleapp:changed
PS D:\Study\DockStudyInVSCode\ExampleApp> docker tag dockerstudy/exampleapp:latest xjdx2008/exampleapp:unchanged

    现在执行docker images发现有四个镜像了。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY               TAG         IMAGE ID       CREATED          SIZE
dockerstudy/exampleapp   changed     4e4417cec9c1   11 minutes ago   210MB
xjdx2008/exampleapp      changed     4e4417cec9c1   11 minutes ago   210MB
dockerstudy/exampleapp   latest      6a3640ffcaf0   18 hours ago     210MB
xjdx2008/exampleapp      unchanged   6a3640ffcaf0   18 hours ago     210MB

     现在将dockerstudy前缀的image删除。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker rmi dockerstudy/exampleapp:changed dockerstudy/exampleapp:latest
Untagged: dockerstudy/exampleapp:changed
Untagged: dockerstudy/exampleapp:latest

PS D:\Study\DockStudyInVSCode\ExampleApp> docker images
REPOSITORY            TAG         IMAGE ID       CREATED          SIZE
xjdx2008/exampleapp   changed     4e4417cec9c1   12 minutes ago   210MB
xjdx2008/exampleapp   unchanged   6a3640ffcaf0   18 hours ago     210MB

    现在,需要登录docker hub,可以通过一下命令登录:

PS D:\Study\DockStudyInVSCode\ExampleApp> docker login -u xjdx2008 -p  ****
WARNING! Using --password via the CLI is insecure. Use --password-stdin.
Login Succeeded

    现在,通过docker push,就可以将本地创建好的镜像推送到docker hub上了。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker push xjdx2008/exampleapp:changed
The push refers to repository [docker.io/xjdx2008/exampleapp]
b8768a70dcaf: Pushed
5f70bf18a086: Pushed
b33e817010b6: Pushed
7dc39ee34889: Pushed
96282ef47aa7: Pushed
82099e0891ed: Pushed
3cf53a68ca2f: Pushed
7e718b9c0c8c: Pushed
changed: digest: sha256:73ce490465c039f6cbd7a7f646dc8659ac90383ecd95d1a8ca7e457763e682dd size: 1997

PS D:\Study\DockStudyInVSCode\ExampleApp> docker push xjdx2008/exampleapp:unchanged
The push refers to repository [docker.io/xjdx2008/exampleapp]
5f70bf18a086: Layer already exists
b33e817010b6: Layer already exists
7dc39ee34889: Layer already exists
96282ef47aa7: Layer already exists
82099e0891ed: Layer already exists
3cf53a68ca2f: Layer already exists
7e718b9c0c8c: Layer already exists
unchanged: digest: sha256:30d23bbd4b70ea808d4b264fde494617c1cea6ed22578daf062dcc05b3e85e7c size: 1788

    第一个docker push会比较慢,是全量上传,第二个就是基于差异化的上传了,很快。

    上传完成之后,在Docker Desktop的 Images/Remote Repository里也可以看到。

    如果在将镜像推送到Docker Hub不添加任何TAG,则默认就是latest。一般我们会默认给一个latest的镜像。

PS D:\Study\DockStudyInVSCode\ExampleApp> docker tag xjdx2008/exampleapp:unchanged xjdx2008/exampleapp:latest
PS D:\Study\DockStudyInVSCode\ExampleApp> docker push xjdx2008/exampleapp:latest
The push refers to repository [docker.io/xjdx2008/exampleapp]
5f70bf18a086: Layer already exists
b33e817010b6: Layer already exists
7dc39ee34889: Layer already exists
96282ef47aa7: Layer already exists
82099e0891ed: Layer already exists
3cf53a68ca2f: Layer already exists
7e718b9c0c8c: Layer already exists
latest: digest: sha256:30d23bbd4b70ea808d4b264fde494617c1cea6ed22578daf062dcc05b3e85e7c size: 1788

     现在可以看到Docker Hub里有三个镜像了。

    完成之后,我们可以登出,使用命令 docker logout

PS D:\Study\DockStudyInVSCode\ExampleApp> docker logout
Removing login credentials for https://index.docker.io/v1/

总结


    本文介绍了如何将一个ASP.NET Core MVC应用程序发布到docker中并运行,另外介绍了docker的基本命令以及基本用法。这里总结一下所有的命令:

//Visual Studio Code快捷格式化
SHIFT+ALT+F

dotnet篇
//创建mvc项目
dotnet new mvc --no-https --output ExampleApp --framework net5.0
//新建globaljson文件
dotnet new globaljson --sdk-version 5.0 --output ExampleApp
//还原
dotnet restore
//运行
dotnet run
//发布
dotnet publish --framework net5.0 --configuration Release --output dist

docker镜像篇
//列出本机镜像
docker images    //-列出所有活动镜像
docker images -q //q表示quite仅列出镜像IMAGEID
//下载镜像
docker pull dockerimageName:tag
//删除镜像 
docker rmi -f IMAGEID
//删除所有镜像
docker rmi -f $(docker images -q)
 
自定义镜像
//新建Dockerfile,内容如下:
FROM mcr.microsoft.com/dotnet/aspnet:5.0  //指定基础镜像
COPY dist /app   //将本机dist文件夹内容拷贝到 镜像的/app文件夹下
WORKDIR /app    //将镜像的app文件夹设置为工作目录
EXPOSE 80/tcp   //暴露镜像中的tcp 80端口
ENTRYPOINT ["dotnet", "ExampleApp.dll"] //设置镜像入口,当镜像启动时,执行的操作。

//创建自定义镜像 .表示当前文件夹,-t表示tag,-f表示docker文件
docker build . -t dockerstudy/exampleapp -f Dockerfile

docker容器篇
//根据镜像创建容器,-p表示指定本机到容器的端口映射 --name指定容器名称,后面跟镜像名称
docker create -p 3000:80 --name exampleapp3000 dockerstudy/exampleapp
//列出所有活动容器
docker ps
//列出所有容器
docker ps -a
//列出所有容器的id
docker ps -aq
//启动容器
docker start exampleapp3000
//启动所有容器
docker start $(docker ps -aq)
//停止容器
docker stop exampleapp3000
//停止所有容器
docker stop $(docker ps -q)
//删除容器
docker rm -f exampleapp3000
//直接基于xjdx2008/exampleapp:changed镜像,运行容器
docker run -p 3000:80  -d xjdx2008/exampleapp:changed
//复制内容
docker cp ./dist/ExampleApp.View.dll exampleapp3000:/app/

//根据容器创建新的镜像
docker commit exampleapp3000 dockerstudy/exampleapp:changed
//根据已有的本地镜像,新建docker hub的用户名为前缀的镜像
docker tag dockerstudy/exampleapp:changed xjdx2008/exampleapp:changed

//推送本地镜像到Docker Hub
docker push xjdx2008/exampleapp:changed
//登录 Docker Hub
docker login -u username -p password
//登出
docker logout

 

参考