Docker Compose 部署 ASP.NET Core

引言

Dockerfile 构建 ASP.NET Core

在添加Docker支持之前,先看本人仓库目录结构:

注意:下方目录结构为本人已经添加完 Docker支持之后的,所以有 Dockerfile文件。

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
50
51
52
53
54
55
│  .dockerignore			# .dockerignore 放在仓库根目录
│ .gitattributes
│ .gitignore
│ CHANGELOG.md
│ deploy-docs.sh
│ LICENSE
│ package-lock.json
│ package.json
│ README.md
│ SimCaptcha.sln # 解决方案文件放在仓库根目录

├─.github
│ └─workflows

├─docs
│ │ README.md
│ │
│ ├─...

├─examples # 示例源代码单独放到 examples 文件夹
│ ├─AspNetCoreClient
│ │ │ AspNetCoreClient.csproj
│ │ │ ...
│ │ │
│ │ ├─...
│ │
│ ├─AspNetCoreService
│ │ │ AspNetCoreService.csproj
│ │ │ ...
│ │ │
│ │ ├─...
│ │
│ └─EasyAspNetCoreService
│ │ Dockerfile # Dockerfile 放在 Project 根目录
│ │ EasyAspNetCoreService.csproj
│ │ ...
│ │
│ ├─...

├─node_modules
│ ├─...

└─src # 所有主要源代码放到 src 文件夹
├─SimCaptcha
│ │ SimCaptcha.csproj
│ │ SimCaptcha.nuspec
│ │ ...
│ │
│ ├─...

└─SimCaptcha.AspNetCore
│ SimCaptcha.AspNetCore.csproj
│ ...

├─...

补充:

生成此路径树使用以下命令(Powershell):

1
tree /f > tree.txt

/f 表示显示每个目录中的文件名

这里不加 /f 也行,加了 /f ,因为有node_modules会导致花费很多时间。

使用 Visual Studio 2019Project 添加 Docker 支持

右键单击目标 Project,点击 Add

选择 Linux,确定完成,生成 Dockerfile如下:

Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#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/core/aspnet:3.1-buster-slim AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

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

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

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

默认此 Dockerfile 就会生成在此Project根目录

注意:

很多同类文章认为 Visual Studio 生成的 Dockerfile 其中的路径不正确,其实不然,只是需要对构建命令做修改,

而不是通常的 docker build -t yiyungent/simcaptcha . ,也不是在此 Project 执行命令,

你应当在根目录执行命令,这样镜像构建上下文(Context)才能复制所有依赖的Project,同时通过指定Dockerfile文件路径的方式构建镜像

注意:

当前由于环境变量的影响,只会监听 80 端口,

郁闷,由于 有 EXPOSE 443 导致误导以为监听了443,结果没有,下面:

1
ENTRYPOINT ["dotnet", "EasyAspNetCoreService.dll",  "--urls", "http://*:80;https://*:443"]

参考:ASP.Net Core 中设置 urls 的方式 · 语雀

注意:

WORKDIR /src 与 仓库文件夹 src 意义不同。

补充:

可以看到,Visual Studio 生成的Dockerfile 中,会自动解析Project依赖关系,从而判断哪些需要复制,但无法解析.csproj中的if方法解析的依赖,所以它都会算作需要依赖,如下:

xxx.csproj
1
2
3
4
5
6
7
<!-- 方便开发debug,与发布到nuget -->
<ItemGroup Condition="'$(Configuration)' == 'Release'">
<PackageReference Include="SimCaptcha.AspNetCore" Version="0.2.0" />
</ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
<ProjectReference Include="..\..\src\SimCaptcha.AspNetCore\SimCaptcha.AspNetCore.csproj" />
</ItemGroup>

上面可看出,实际Release时,不需要对 SimCaptcha.AspNetCore 对Project依赖,但Dockerfile中还是生成了。

当你添加Docker支持的Project有Project依赖时,你应保证,所有Project依赖都被复制,并保持相对路径不变。

补充:

上面的这种构建方式名为多阶段构建。

构建镜像

注意:此命令在 仓库根目录执行(即解决方案同级目录)

1
docker build -t yiyungent/simcaptcha -f examples/EasyAspNetCoreService/Dockerfile .

补充:

其实关键在于构建上下文,因此,如果在 Project 根目录执行构建的话,下方命令也可以:

记住,默认 -f 指定Dockerfile位置,为 构建上下文同级目录Dockerfile

1
docker build -t yiyungent/simcaptcha -f Dockerfile ../..

其实,在 Visual Stuido 中添加 Docker 支持时,你会发现会修改 xxx.csproj,如下:

xxx.csproj
1
2
3
4
5
6
7
8
9
10
11
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyName>EasyAspNetCoreService</AssemblyName>
<UserSecretsId>1955bcda-6f7b-491c-9fab-0e7e7a2cdb45</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerfileContext>..\..</DockerfileContext>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
</ItemGroup>

其中,<DockerfileContext>..\..</DockerfileContext> 中就是对 上下文的设置,

如果需要在Visual Studio 中Debug Docker,

那么请务必使用Visual Studio 添加Docker支持,而不是自己写Dockerfile,

因为 Visual Stuido 添加Docker 支持不仅仅是添加了Dockerfile,还有 launchSettings.jsonEasyAspNetCoreService.csproj.dockerignore

Properties/launchSettings.json

Properties/launchSettings.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"EasyAspNetCoreService": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5003;http://localhost:5004"
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": true
}
}
}

添加HTTPS,参考:

补充

简单版 Dockerfile 构建 ASP.NET Core

一般一个解决方案下有多个项目(Project),可能不止一个项目(Project)需要使用Dockerfile构建镜像,所以不建议放在仓库根目录(与解决方案文件同级),而 .dockerignore 文件用于忽略不需要加入构建镜像的文件,默认需要与 Dockerfile同级目录。

注意:应保证复制完当前Project所有 ProjectReference(PackageReference不需要,因为会从网络下载),并保证相对路径不变

下面使用标准的 ASP.NET Core Runtime,以及 SDK 构建镜像

Dockerfile 放在项目文件(EasyAspNetCoreService.csproj)同级目录

Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

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

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

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

补充:

之所以可以像这样做,是因为此 Project对于Release而言,无Project依赖,只需此Project源代码即可构建

此种方式,直接在此Project执行构建镜像命令即可。

.dockerignore

放在与 Dockerfile 同级目录,用于忽略不需要加入构建镜像的文件

.dockerignore
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
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

上方内容由 Visual Studio 2019 自动生成,其实当放在Project级别目录时,很多忽略内容都不再需要,例如:**/.git,但这里还是保留,以备不时之需。

docker 命令中环境变量指定urls

参考:

1
docker run -d -p 5004:80 -p 5003:443 -e ASPNETCORE_URLS="http://*:80;https://*:443" --name simcaptcha-container yiyungent/simcaptcha

值得注意的是,使用HTTPS,需要证书:

Docker下 | System.Net.WebException: Cannot assign requested address

源自笔者项目:https://github.com/yiyungent/SimCaptcha/issues/8

初步判定是因为容器之间彼此隔离,无法通信导致 客户端容器 无法 HTTP 请求 验证码服务端容器,

参考:

System.Net.WebException: Cannot assign requested address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
System.Net.WebException: Cannot assign requested address Cannot assign requested address
---> System.Net.Http.HttpRequestException: Cannot assign requested address
---> System.Net.Sockets.SocketException (99): Cannot assign requested address
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
--- End of inner exception stack trace ---
at System.Net.Http.ConnectHelper.ConnectAsync(String host, Int32 port, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.ConnectAsync(HttpRequestMessage request, Boolean allowHttp2, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.CreateHttp11ConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.GetHttpConnectionAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpConnectionPool.SendWithRetryAsync(HttpRequestMessage request, Boolean doRequestAuth, CancellationToken cancellationToken)
at System.Net.Http.RedirectHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.DiagnosticsHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.FinishSendAsyncUnbuffered(Task`1 sendTask, HttpRequestMessage request, CancellationTokenSource cts, Boolean disposeCts)
at System.Net.HttpWebRequest.SendRequest()
at System.Net.HttpWebRequest.GetResponse()
--- End of inner exception stack trace ---
at SimCaptcha.Common.HttpAide.HttpPost(String url, String postDataStr, StringBuilder responseHeadersSb, String[] headers, WebProxy proxy) in /src/src/SimCaptcha/Common/HttpAide.cs:line 184
at SimCaptcha.SimCaptchaClient.Verify(String ticket, String userId, String userIp) in /src/src/SimCaptcha/SimCaptchaClient.cs:line 84

解决: 将两个容器处于同一 network(bridge)即可,再通过容器名访问,例如: simcaptcha-container:80

Docker下 System.Drawing.Image.FromFile 不可用

源自笔者项目:https://github.com/yiyungent/SimCaptcha/issues/6

参考:

1
2
3
4
5
6
7
8
9
10
11
System.TypeInitializationException: The type initializer for 'Gdip' threw an exception.
---> System.DllNotFoundException: Unable to load shared library 'libgdiplus' or one of its dependencies. In order to help diagnose loading problems, consider setting the LD_DEBUG environment variable: liblibgdiplus: cannot open shared object file: No such file or directory
at System.Drawing.SafeNativeMethods.Gdip.GdiplusStartup(IntPtr& token, StartupInput& input, StartupOutput& output)
at System.Drawing.SafeNativeMethods.Gdip..cctor()
--- End of inner exception stack trace ---
at System.Drawing.SafeNativeMethods.Gdip.GdipLoadImageFromFile(String filename, IntPtr& image)
at System.Drawing.Image.FromFile(String filename, Boolean useEmbeddedColorManagement)
at System.Drawing.Image.FromFile(String filename)
at SimCaptcha.AspNetCore.AspNetCoreVCodeImage.Create(String code, Int32 width, Int32 height) in /src/src/SimCaptcha.AspNetCore/AspNetCoreVCodeImage.cs:line 54
at SimCaptcha.SimCaptchaService.CreateVCodeImg() in /src/src/SimCaptcha/SimCaptchaService.cs:line 484
at SimCaptcha.SimCaptchaService.VCode() in /src/src/SimCaptcha/SimCaptchaService.cs:line 408

文件位置: src/SimCaptcha.AspNetCore/AspNetCoreVCodeImage.cs:line 54

1
var imageStream = Image.FromFile(randomImgFile);

解决:

Dockerfile
1
2
3
# 解决 Linux 下缺少 'libgdiplus'
RUN apt-get update
RUN apt-get install -y --no-install-recommends libgdiplus libc6-dev

Docker下 中文无法显示 | 中文乱码

源自: https://github.com/yiyungent/SimCaptcha/issues/7

参考:

解决:

微软 base 镜像问题,自己构建了个base镜像就好了:

https://github.com/yiyungent/dotnet-docker/blob/main/aspnetcore-runtime-3.1.Dockerfile

aspnetcore-runtime-3.1.Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# https://docs.microsoft.com/zh-cn/dotnet/core/install/linux-ubuntu#2004-

FROM ubuntu:20.04 AS base

LABEL maintainer="yiyun <yiyungent@gmail.com>"

# 设置国内阿里云镜像源
COPY etc/apt/aliyun-ubuntu-20.04-focal-sources.list /etc/apt/sources.list

RUN apt-get update
RUN apt-get install -y wget

# 将 Microsoft 包签名密钥添加到受信任密钥列表,并添加包存储库
RUN wget https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
RUN dpkg -i packages-microsoft-prod.deb
# 安装运行时
RUN apt-get update
RUN apt-get install -y apt-transport-https
RUN apt-get update
RUN apt-get install -y aspnetcore-runtime-3.1

# 时区设置
ENV TZ=Asia/Shanghai

Windows下 Docker --network host 无效

参考:

The host networking driver only works on Linux hosts, and is not supported on Docker for Mac, Docker for Windows, or Docker EE for Windows Server.

Docker Ubuntu 安装 ping

1
docker exec -it <容器ID> bash
1
apt-get update
1
apt-get install -y iputils-ping
1
ping 容器名

CentOS 安装 Git

1

1
yum install -y git

Nginx 反向代理

nginx.conf
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
server {
listen 80;
server_name captcha.moeci.com; # 绑定的域名,例如:www.example.com。多个域名用空格分开。
access_log off;

# 反向代理: 将请求转发到 ASP.NET Core 端口 5004
location / {
proxy_pass http://localhost:5004;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

server {
listen 80;
server_name captcha-client.moeci.com; # 绑定的域名,例如:www.example.com。多个域名用空格分开。
access_log off;

# 反向代理: 将请求转发到 ASP.NET Core 端口 5002
location / {
proxy_pass http://localhost:5002;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

参考

感谢帮助!