在Docker中运行.NET 应用,初学者教程-在Docker容器中运行NancyFx


【编者的话】本文详细讲解了如何在Docker容器中运行.NET应用,这也说明了容器技术的一种趋势,即使像微软这样的大企业也在逐步向Docker靠拢。

在安静的圣诞节期间来研究在我列表上有一段时间了的新技术和最近趋势是一个好时机。这个圣诞节我花时间学习了最新的ASP.NET框架,尤其是通过CoreCLR将ASP.NET 5应用部署在Linux操作系统上,还有通过mono在Docker 容器中运行一个普通的.net 4.x的Web框架。后一种技术是我今天这篇文章的主题。

什么是Docker

我假设你对Docker有一个基本的认识,了解Docker革新了我们在云中托管软件的技术,清楚容器比虚拟机的好处。如果对这些并不理解,那么我强烈推荐你先熟悉容器的基本概念,知道为什么在容器中部署应用是值得的。

下面是一些让你开始的资源:


在Windows上安装Docker

首先我在本地安装Docker,这样我可以在开发环境调试应用。幸运的是这对我们来说非常容易。我需要做的就是下载Windows版的Docker Toolbox,然后根据指示安装它。

Docker Toolbox

在我安装完成后我会安装以下三个软件


如果你已经安装了VirtualBox,这部可以跳过了。最重要的是VirtualBox提供了一个对外接口,来方便其他应用自动管理虚拟机。这也是Docker Machine的做法。它会在VirtualBox中创建一个新的虚拟机,这个镜像里面包含了启动Docker需要的一切。因为这一切都是自动的,所以你并不需要担心VirtualBox。

Kitematic是一个关于Docker Machine的图形化客户端。它的功能非常有限,所以你也许并不需要它。

最后一个应用就是Docker Terminal,我们用它来做的唯一一件事就是在本地环境启动和管理Docker容器。

在终端中启动你的第一个Docker命令

在成功安装之后让我们执行第一条Docker命令来看看是否奏效。在你第一次打开终端的时候它会在VirtualBox中初始化虚拟机。这会花费一些时间,但是最后会以这样的屏幕来结束:
01.jpg

你不需要打开Kitematic或Virtual来运行它。正如我在前面说的,你可以开心的忽略这两个软件,然而,如果你仔细的话,打开VirtualBox你会看到虚拟机正如期望的那样运行着。
02.jpg

这是一个通过boot2docker.iso启动的linux镜像。

返回到终端,我们可以通过输入命令docker ersion来看到关于Docker客户端和服务端应用的一些基础的版本信息。
03.jpg

如果看到上面的信息,说明Docker运行正常。

这里有一个信息需要着重说明一下,那就是在Docker终端里面的初始化信息。
04.jpg

在终端中显示的IP地址是这篇文章中你获取你自己应用的节点。

为Docker创建一个NancyFx的Web应用

现在是时候创建一个可以在Mono上运行的.Net web应用了。

首先我用一个常规的cosole application模版来新建一个工程,应用到 .NET Framework 4.6.1。

这个工程除了Program.cs这个文件以外其他都是空的:
class Program
{
static void Main(string[] args)
{
}
}  

下面我会安装3个NuGet packages:
Install-Package Nancy
Install-Package Nancy.Hosting.Self
Install-Package Mono.Posix

第一个package安装NancyFx web框架。Nancy是一个用于创建Http基础服务的轻量级.NET框架。你可以把它当作ASP.NET,但是它和ASP.NET、IIS和System.Web namespace无关。

你依然可以将Nancy应用部署在IIS上面,但是对应的你可以在某处像管理终端应用一样管理它。这正是我们将要做的,也是我们安装Nancy.Hosting.Self作为第二个安装包的原因。

第三个包安装了POSIX interface for Mono and .NET。

在安装完Nancy包后我现在可以配置一个节点,并启动一个新的Nancy.Hosting.Self.NancyHost
using System;
using Nancy.Hosting.Self;

class Program
{
static void Main(string[] args)
{
    const string url = "http://localhost:8888";

    var uri = new Uri(url);
    var host = new NancyHost(uri);

    host.Start();
}
}  

这个终端应用会在启动后马上结束,因此我需要添加一些东西来保持它的运行状态,比如Console.ReadLine()命令。另外我想在应用关闭前停止宿主机:
host.Start();
Console.ReadLine();
host.Stop();  

如果我想将这个程序运行在windows上,那我已经完成了,但是在linux上我想用Unix的终端信号来替代。

下面是用一种帮助方法来检测应用是否在Mono上运行:
private static bool IsRunningOnMono()
{
return Type.GetType("Mono.Runtime") != null;
}  

另一种方法是暴露Unix的中断信号:
private static UnixSignal[] GetUnixTerminationSignals()
{
return new[]
{
    new UnixSignal(Signum.SIGINT),
    new UnixSignal(Signum.SIGTERM),
    new UnixSignal(Signum.SIGQUIT),
    new UnixSignal(Signum.SIGHUP)
};


我把两种方法都加在了我的Program class,为了同时支持Windows和Unix中断,修改了Main方法。
host.Start();

if (IsRunningOnMono())
{
var terminationSignals = GetUnixTerminationSignals();
UnixSignal.WaitAny(terminationSignals);
}
else
{
Console.ReadLine();
}

host.Stop(); 

这是最终方法的样子:
using System;
using Nancy.Hosting.Self;
using Mono.Unix;
using Mono.Unix.Native;

class Program
{
static void Main(string[] args)
{
    const string url = "http://localhost:8888";

    Console.WriteLine($"Starting Nancy on {url}...");

    var uri = new Uri(url);
    var host = new NancyHost(uri);
    host.Start();

    if (IsRunningOnMono())
    {
        var terminationSignals = GetUnixTerminationSignals();
        UnixSignal.WaitAny(terminationSignals);
    }
    else
    {
        Console.ReadLine();
    }

    host.Stop();
}

private static bool IsRunningOnMono()
{
    return Type.GetType("Mono.Runtime") != null;
}

private static UnixSignal[] GetUnixTerminationSignals()
{
    return new[]
    {
        new UnixSignal(Signum.SIGINT),
        new UnixSignal(Signum.SIGTERM),
        new UnixSignal(Signum.SIGQUIT),
        new UnixSignal(Signum.SIGHUP)
    };
}
}  

现在我只缺少一个服务Http请求的Nancy模块。通过从Nancy.NancyModule中实现一个新的模块,并且将它注册到至少一个路由可以实现。我在/目录下建立了一个“Nancy: Hello World”消息,并在/os节点下建立了一个操作系统版本的消息。
using System;
using Nancy;

public class IndexModule : NancyModule
{
public IndexModule()
{
    Get["/"] = _ => "Nancy: Hello World";
    Get["/os"] = _ => Environment.OSVersion.ToString();
}


如果我编译并运行这个应用,我会在访问 http://localhost:8888时看到hello world 信息,在访问http://localhost:8888/os时看到系统的版本信息:
05.jpg

在Docker容器中运行NancyFx

这个应用很简单但是足够在Docker容器中运行第一个版本。

创建Dockerfile

首先我需要构建一个包含应用和它所有依赖的Docker镜像。为了这个目的,我要创建一个定义了在镜像中运行的应用程序的方法。这个方法就是Dockerfile,这是一个包含如何组成镜像的可读指令集合。重要的是完全按显示命名,不要包含文件扩展和大写的“D”。

将Dockfile加入到你的工程是一个很好的练习,因为当你的工程改变时,Dockfile也要跟着更改。
06.jpg

我想将Dockerfile加入到应用的输出中,所以我需要将“Build Action”设置为“Content”,将“Copy to Output Directory”设置为“Copy always”。
07.jpg

Visual Studio 2015 在创建文件时默认使用 UTF-8-BOM 编码。这会在创建的文件首行添加一个不可见的BOM字符,这在通过Dockerfile构建镜像时会引起错误。最简单的解决办法是通过 Notepad 打开文件,并将编码格式修改为UTF-8。
08.png

你也可以通过设置Visual Studio来改变保存文件的编码。

现在我可以定义并排序生成步骤。

每个Dockerfile文件都以FROM指令开始。这定义了基本镜像开始的位置。Dokcer镜像轻量的一个原因就是Docker使用了layering system。你可以在Docker Hub上找到很多官方镜像来开始。

幸运的是,这里已经有一个官方的Mono仓库可供我们使用。在我写作的时候最新的镜像是4.2.1.102。正如你看到的一样,Mono镜像使用Debian的官方仓库中的debian:wheezy image作为它的基本镜像。这个Debian镜像使用空的scratch镜像作为它的基础。当我们使用Mono镜像的时候我们需要在目录的顶端重新构建一个层次结构:
scratch
\___ debian:wheezy
   \___ mono:4.2.1.102
       \___ {our repository}:{tag}  

如果你观察Mono的官方仓库你会看到最新的Mono镜像有很多标签:
09.jpg

通过你的用例来选择最合适你应用的版本。尽管它们都是由同一个Dockerfile构建的,但是只有4.2.1.102是始终保证完全相同的构建。
从个人角度来说我会选择这个版本作为我的生产应用:
FROM mono:4.2.1.102

下面的两个命令非常明了。我想要创建一个名字为/app的文件夹,将所有与执行这个应用相关的文件复制进去。记住Dockerfile会被复制到输出文件夹下。这意味着同一目录下的所有文件都需要被拷贝到/app文件夹下。
RUN mkdir /app
COPY . /app

我的Nancy应用配置为监听8888端口。通过使用EXPOSE命令提示容器监听指定端口:
EXPOSE 8888

最后我将应用部署到Mono上:
CMD ["mono", "/app/DockerDemoNancy.Host.exe", "-d"] 

最后的Dockerfile如下所示:
FROM mono:4.2.1.102
RUN mkdir /app
COPY . /app
EXPOSE 8888
CMD ["mono", "/app/DockerDemoNancy.Host.exe", "-d"]

利用Dockerfile你还可以进行很多操作。参考Dockerfile reference查看完整的指令列表。

构建Docker镜像

构建一个Docker镜像非常简单。通过Docker终端查看我的Nancy应用下面的/bin/Realease目录:
cd /c/github/docker-demo-nancy/dockerdemonancy.host/bin/release

下面我运行 docker build命令,并用-t给镜像添加标识:
docker build -t docker-demo-nancy:0.1.0 .

不要忘了以句号结尾。这指明了包含Dockerfile的目录。因为我已经在/bin/Realease目录下,所以我只是以句号结尾。

构建的过程会执行每个指令,并且在执行之后会生成新的目录结构。在你第一次执行命令的时候你的硬盘上不会有mono:4.2.1.102镜像,Docker会自动从公有仓库(Docker Hub)下载下来。
10.jpg

正如你看到的,FROM指令会下载六个不同的镜像,这事因为mono:4.2.1.102镜像和它的继承镜像(debian:wheezy)一共有六条指令,所以产生了六层镜像。

将这些过程可视化的方法是监控我们自己的镜像。

一旦构建完成,我们可以通过docker image命令来查看镜像列表:
11.jpg

通过docker history {image-id}可以查看镜像的全部历史记录,包括这个镜像在哪一层,执行过哪些指令。
12.jpg

这样非常智能。无论如何,我们刚刚创建了我们的第一个Docker镜像。
如果你想将镜像上传到Docker Hub或者其他私有仓库,你可以使用docker tag给已有的镜像打上标签,然后利用docker push命令将它上传到仓库。

创建并运行第一个Docker应用

运行一个Docker容器并不容易。用docker run 命令来创建并运行一个容器:
docker run -d -p 8888:8888 docker-demo-nancy:0.1.0

-d参数说明Docker以独立模式运行,-p 8888:8888参数将容器的8888端口与主机8888端口对应起来。

然后你可以用命令docker ps来查看正在运行的容器:
13.jpg

非常好,现在将{docker-ip}:8888粘贴到浏览器地址栏,你将会看到hello world信息:
14.jpg

浏览{docker-ip}:8888/os会看到“Unix 4.1.13.2”:
15.jpg

这非常棒。我们毫不费力的在Docker容器中通过Mono部署了一个 Nancy .Net应用。


提示:将你Docker的IP地址映射到一个友好的DNS。
你可以通过更改Windows的hosts文件将Dokcer的IP地址映射到DNS:
  1. 以管理员的身份打开C:\Windows\System32\drivers\etc\hosts
  2. 映射IP地址192.168.99.100 docker.local
  3. 保存文件


现在你可以在地址栏输入docker.local:8888来直接获取返回信息:
16.jpg

通过Docker来配置指定的环境设置

在这篇文章的最后我想要说明如何在Docker容器中控制环境变量。

我认为当你迁移你的Docker容器时,你不能修改你的Docker镜像。这意味着在Docker镜像中的app.config文件在每个环境都是一样的。

人们习惯传换配置文件。这种习惯需要停止,Docker使得在容器启动时配置参数变得容易。

让我们对Nancy的IndexModule做出一点小的改变:
public IndexModule()
{
var secret = Environment.GetEnvironmentVariable("Secret");

Get["/"] = _ => "Nancy: Hello World";
Get["/os"] = _ => Environment.OSVersion.ToString();
Get["/secret"] = _ => secret jQuery2110766658102395013_1454232198306 "not set";


这是个很明显的改变。我将一个名为“Secret”的参数加入到环境变量中,并将它公开出来。

环境变量可以是任何数据,但通常来说他包含着加密密匙、数据库连接串和诸如错误日志路径这样的敏感数据。

这个例子的目的只是说明可以将环境变量公开。

现在我需要运行之前的指令来重新编译并构建应用。我将新的镜像标记为docker-demo-nancy:0.2.0。

为了防止端口冲突,我需要将之前运行的Docker容器先停下来,尽管我很希望能同时运行这两个容器。
docker run -d -p 8888:8888 -e Secret=S3cReT docker-demo-nancy:0.2.0

docker run命令通过-e 参数可以指定很多环境变量。还有很多不同的方法可以指定环境变量,但是你可能最想用到是通过--env-file参数来导入额外的变量。

这样做有以下几个好处:
1. 你可以轻易地传递环境变量
2. 你可以轻易地提供很多环境变量
3. 敏感数据不会出现在日志文件中
4. 日志文件存在指定目录可以在生产环境中更好的安排执行计划

在加载了secret变量的容器启动之后,我可以通过运行docker inspect {container-id} 命令来获取容器相关的大量数据。下面是容器加载的变量信息:
17.jpg

浏览 docker.local:8888/secret 会看到环境变量的数据:
18.jpg

概述

我第一篇关于在Docker中运行.NET应用就这样结束了。我希望我能撇去Docker的基础知识,将关注点放在如何更快、更容易的通过Docker部署.NET应用上。

在这个例子中我使用Nancy框架来部署一个web应用,但是我同时可以使用一个可以在Mono或者ASP.NET 5上部署的普通.NET应用,这是可以跨平台的。

在这篇文章中还有很多在Docker中运行.NET应用的知识点我没有提到。这些事包括在Docker容器中调试应用,通过你的CI来构建Docker镜像,在生产环境中管理容器等。请关注我下面的系列文章,我会在这系列的文章中深入的探讨这些问题。

完整的示例代码可以在GItHub上找到。镜像也可以在我的Docker Hub仓库上下载下来。

原文链接:Running NancyFx in a Docker container, a beginner's guide to build and run .NET applications in Docker(翻译:邢毅勋)

===========================================================================
译者介绍
邢毅勋,亚信研发工程师,热爱开源好青年。

1 个评论

可以试试OneAPM ,助您轻松锁定 .NET 应用性能瓶颈,通过强大的 Trace 记录逐层分析,直至锁定行级问题代码。以用户角度展示系统响应速度,以地域和浏览器维度统计用户使用情况。可以在官网注册试用哦~

要回复文章请先登录注册