利用Dockerfile将ASP.NET Core Web应用部署到Docker容器

前言

想必各位对ASP.NET Core容器化部署,特别是微软非常喜欢强调的 微服务(Micro-Service) 概念有一定的兴趣。

这一过程需要使用到ASP.NET Core, Docker, Nginx三个组件,要让它们相互良好地配合起来,还是需要精心编写一下配置文件的。

然而,当真的开始着手实操的时候,假如阅读微软文档给出的一些文章或者链接,例如:

或许对于幼儿园的小朋友来说,这些文章可能显得比较幼稚。但对我这种饭来张口衣来伸手的大学生来说,这些文章只能说刚刚好。把文章拼起来就是需要的答案,可惜在尝试的过程中会显得有些不适。所以不妨总结一下,如何将一个ASP.NET Core Web应用部署到Docker中,然后用Nginx做反向代理进行访问。

准备

  • 已经开发好的ASP.NET Core Web应用程序(Razor Pages, Blazor, MVC, Web API啥的,都可以)
  • Docker
  • Nginx

这里,我们会使用最新的.NET 6.0进行部署。

ASP.NET Core代码的适配

在本教程中,我们由于需要使用Nginx配置反向代理,这需要我们对ASP.NET Core应用进行响应的修改。

关闭HTTPS重定向

ASP.NET Core项目默认会重定向所有HTTP请求到HTTPS,但在反向代理的场景下,HTTPS不是容器中的ASP.NET Core程序该操心的事情。我们应该在Nginx这样的Web服务器部分完成HTTPS相关的配置,包括HTTP重定向HTTPS。

打开Program.cs,找到如下代码:

Program.cs
1
app.UseHttpsRedirection();

app.UseHttpsRedirection()一句删除或注释掉,关闭HTTPS重定向。

设置反向代理使用的响应标头

为了能正确跟踪请求的来源,处理重定向、认证等关键的事件,我们还需要配置转发响应标头。

同样是在Program.cs中,我们在其他中间件之前,加入UseForwardedHeaders代码段:

Program.cs
1
2
3
4
5
6
7
8
9
10
......
var app = builder.Build();

+ app.UseForwardedHeaders(new ForwardedHeadersOptions
+ {
+ ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
+ });

if (app.Environment.IsDevelopment())
......

关于中间件先后顺序的讨论,可以参考ASP.NET Core Middleware | Microsoft Docs.

配置步骤

第1步-创建文件夹

创建一个新文件夹,用来存放构建镜像所需的文件,包括Dockerfile等。我们假设这个新文件夹名为myapp

第2步-发布ASP.NET Core应用

这一步我们要将编写的ASP.NET Core应用发布,生成生产环境下的二进制代码。

打开终端,切换到项目根目录,运行:

1
dotnet publish -c Release -o publish --no-restore

其中-o publish的意思是将发布的二进制代码放入名为publish的文件夹中。发布完成后,进入该文件夹,可以找到与当前项目同名的一个.dll文件,这就是你的项目编译生成的二进制代码。我们这里假设该文件名为MyWebApp.dll

发布完成后,将publish文件夹复制到myapp文件夹中。

第3步-编写Dockerfile

myapp文件夹中,新建一个Dockerfile文件,编写内容:

Dockerfile
1
2
3
4
FROM mcr.microsoft.com/dotnet/aspnet:6.0
WORKDIR /app
COPY publish statics ./
ENTRYPOINT ["dotnet", "MyWebApp.dll"]

依次解释上述语句含义:

  • FROM mcr.microsoft.com/dotnet/aspnet:6.0指定了基础的镜像文件,我们选用的是微软发布的最新ASP.NET Core Runtime镜像,不包含SDK
  • WORKDIR /app指定接下来容器中的操作,基础目录是/app
  • COPY publish statics ./会将 本机publish文件夹下的所有文件 复制到 容器当前目录下
  • ENTRYPOINT ["dotnet", "MyWebApp.dll"]放在最后,容器将执行这一命令,即通过dotnet命令运行上一行语句中复制到/app目录下的MyWebApp.dll文件。这将会保持该Web程序一直运行,直到被要求退出。

第4步-构建并运行Docker镜像

myapp文件夹中,运行命令:

1
docker build -t mywebapp .

-t mywebapp指定了生成镜像的标签(tag)为mywebapp,当然你也可以写成mywebapp:0.1.2这样的形式来指定一个版本号。

运行命令,Docker会拉取最新的ASP.NET Core Runtime镜像,然后执行我们在Dockerfile中指定的操作,构建成一个新的镜像,放在系统中。

接下来我们就可以启动我们的应用了。执行命令:

1
docker run -d -p 12345:80 --name mywebapp-123 mywebapp

参数的解释:

  • -d表示容器在后台运行(daemon)
  • -p 12345:80表示将主机的12345端口映射到容器的80端口上,即可以通过主机的12345端口直接访问容器内ASP.NET Core应用。由于我们需要设置反向代理,出于安全考虑,我们可以设置只映射来自本机的访问,即写成-p 127.0.0.1:12345:80这样的形式,此时即便没有防火墙,外界直接访问12345端口也无法访问到我们的ASP.NET Core应用程序。
  • --name mywebapp-123,给你的容器起一个名字。
  • mywebapp,选择运行容器使用的镜像Tag。(和上面容器名字可以相同,也可以不同,但这个名字来源于上面构建Docker镜像中指定的Tag,如果设置了版本号,还应当指明版本号。)

如果没有意外情况,执行上述命令后,我们的应用就运行在机器上了。可以通过docker ps查看正在运行的容器,看我们刚刚建立的容器是否正常运行。

第5步-设置反向代理

在现代的网站中,我们不是很喜欢通过端口号访问网站,更多情况下,我们需要借助如Nginx等Web服务器的反向代理等功能,完成更复杂的工作。因此,我们现在看看怎么使用Nginx为我们的应用设置反向代理。

再次确认

执行本步骤前,请确保容器中运行的程序已经按照 ASP.NET Core代码的适配 部分修改并生成。

这里,我们使用Debian/Ubuntu系Linux发行版的配置文件目录习惯,如果使用的是Fedora/RedHat系发行版,需要自行确定下Nginx配置文件的目录,并创建对应的文件。

我们在/etc/nginx/sites-available/下创建一个新的站点配置文件,可以起名为myapp.conf。编写如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;
listen [::]:80;

server_name myapp.example.com;

location / {
proxy_pass http://127.0.0.1:12345;
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;
}
}

其他部分大同小异,我们主要需要注意的是以下几点。

  • 如果你的服务器上还运行了其他域名的应用,请确认server_name后正确指定了需要转发给ASP.NET Core应用程序的域名,本例中为myapp.example.com
  • proxy_pass后需要正确指定 第4步-构建并运行docker镜像 中映射到容器80端口的本机端口号

(可选)第6步-设置HTTPS重定向

现在绝大部分网站都已经使用了HTTPS,我们为了安全理应同样使用HTTPS。当然,这里我们不讨论如何获得HTTPS证书,我们稍微提一提怎么做重定向。

将第五步中的Nginx配置文件修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name myapp.example.com;

ssl_certificate /etc/ssl/certs/myapp.crt;
ssl_certificate_key /etc/ssl/private/myapp.key;
ssl_dhparam /etc/ssl/certs/myapp.pem;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
ssl_ecdh_curve secp384r1;
ssl_prefer_server_ciphers on;

ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

location / {
proxy_pass http://127.0.0.1:12345;
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;
}
}

server {
listen 80;
listen [::]:80;

server_name myapp.example.com;

root /var/www/html;

if ($host = myapp.example.com) {
return 301 https://$host$request_uri;
}

return 404;
}

上述Nginx的TLS配置仅供参考。我们选取了一个相对较为安全的配置。

第7步-启动Nginx并测试访问

执行命令:

1
nginx -s reload

Nginx将会重新读取配置文件,设置好DNS解析(如果使用了域名的话),就可以通过绑定的域名访问到容器中的ASP.NET Core Web应用了。