让我的 .NET Core 博客系统支持 Docker
导语
我的博客(https://edi.wang)所使用的博客系统 Moonglade 开源已经一年多了。目前已有至少4位社区朋友使用此系统在 Azure、阿里云上部署了自己的博客。可惜长久以来该系统一直缺乏 Docker 支持,而 .NET Core 必须结合 Docker 才是当今世界的政治正确。我作为一名20年的老软粉,虽然嘴上说着很不情愿用 Linux、Docker这种非微软的东西,但也只能假装抱着批判的态度,向 Linux 和 Docker 伸出了魔爪,让我的博客系统能够容器化运行。
Docker 环境安装
我作为一个20多年的老软粉,怎么可以在自己纯洁的 Windows 电脑上装 Docker 呢?装完以后:Docker 真香。
为了最大限度的避免 Windows 被污染(尽管它已经是咖喱拌饭了),我的 Docker 编译和发布环境都配置在云端,采用 Azure DevOps + Docker Hub + Azure App Service Linux Plan 的方式去编译运行。
Dockerfile
Visual Studio 可以直接右键一个 ASP.NET Core 项目添加 Docker 支持,这种方式可以让你很方便的在本地调试 Docker 中的 ASP.NET Core 程序。VS除了向工程目录添加一个 Dockerfile 以外,还会修改你的 csproj 工程文件,好让工具链整合你的容器。而其实对于单纯编译和运行 ASP.NET Core 网站而言,单独一个 Dockerfile 就够了,Docker 会根据这个 Dockerfile 编译出应用的容器镜像。
最初我博客的 Dockerfile 内容如下:
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 ["Moonglade.Web/Moonglade.Web.csproj", "Moonglade.Web/"]
# COPY 其余的工程文件,篇幅关系省略
RUN dotnet restore "Moonglade.Web/Moonglade.Web.csproj"
COPY . .
WORKDIR "/src/Moonglade.Web"
RUN dotnet build "Moonglade.Web.csproj" -p:Version=10.2.0-docker -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "Moonglade.Web.csproj" -p:Version=10.2.0-docker -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "Moonglade.Web.dll"]
这份 Dockerfile 和 Visual Studio 自动生成的没太大区别。其中,我指定编译版本号参数为 -p:Version=10.2.0-docker,以便于直接从博客网站的界面分辨部署类型是 Docker 还是传统的 Code。
YAML
在 Azure DevOps 上,我使用 YAML 方式编译和部署我的博客项目,其中 Docker 的编译步骤定义如下:
- job: Docker
pool:
vmImage: 'ubuntu-latest'
steps:
- task: Docker@2
displayName: 'Build and Push'
inputs:
containerRegistry: 'ediwang_dockerhub'
repository: ediwang/moonglade
tags: latest
由于VM镜像选的是 Ubuntu,因此这个 Docker 镜像编译出来为 Linux/x64 架构。如果你需要其他架构,可以自行添加其他类型的VM镜像。
ediwang_dockerhub 是预先在 Azure DevOps 授权配置好的针对 Docker Hub 的连接名称。编译完成后,Azure DevOps 会使用其中的授权向 Docker Hub 发布镜像。
福报#1:路径问题
当我兴高采烈的测试我的 Docker 容器时,我惊喜的发现,博客的博主头像、RSS订阅、OPML等全部都404了。根据之前我修过的Linux福报,我立即明白这是路径写法的问题。
在 Windows 系统中,表示一个文件或文件夹的路径通常用反斜杠分割目录,如:
C:\Fubao\996.icu
而 Linux 系统中,路径得用斜杠来分割目录,如:
/use/dotnet/work/955
像我这样的老牌软狗,很容易按照习惯把代码写成 Windows 的形式,毕竟微软曾经说好的 Linux 是毒瘤, .NET 只能在 Windows 上跑:
var fallbackImageFile = $@"{AppDomain.CurrentDomain.GetData(Constants.AppBaseDirectory)}\wwwroot\images\default-avatar.png";
其实 .NET 自古以来都有个API:Path.Combine(),用来拼路径,它在 .NET Core 里遇到 Linux 环境可以正确使用斜杠,于是软狗以为这样写就没事了:
var cssPath = Path.Combine(webRootPath, "css", "theme", currentTheme);
大部分情况确实是好的,然而我们来看个会爆的例子:
var p1 = "/dotnet";
var p2 = "/fubao/996.icu";
Path.Combine(p1, p2);
猜猜结果变成什么?
/dotnet 被丢掉了,只能996,进ICU。
好在微软为了不让我们进ICU,在.NET Standard 2.1里引入了 Path.Join() 方法,可以输出我们想要的结果:
因此,我把博客代码里用到路径的地方全部都用 Path.Join() 改了一遍,终于恢复了博主头像、RSS等资源的正常访问。
Path.Join() 参考文档:https://docs.microsoft.com/en-us/dotnet/api/system.io.path.join?view=netcore-3.1
福报#2:libgdiplus
博客程序运行期间,还报了另一个错,日志如下:
2020-03-31T12:02:53.405115468Z System.TypeInitializationException: The type initializer for 'Gdip' threw an exception.
2020-03-31T12:02:53.405359877Z ---> 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
2020-03-31T12:02:53.405375177Z at System.Drawing.SafeNativeMethods.Gdip.GdiplusStartup(IntPtr& token, StartupInput& input, StartupOutput& output)
2020-03-31T12:02:53.405390778Z at System.Drawing.SafeNativeMethods.Gdip..cctor()
2020-03-31T12:02:53.405397878Z --- End of inner exception stack trace ---
这是由于博客代码用到了一些 .NET Core 的绘图 API,而这些 API 的底层需要 Linux 系统上装一个叫做 libgdiplus 的库。可是 Azure App Service 的 Linux 容器主机对用户来说无法直接操作,不可能 SSH 进去给它装个库,怎么办呢?
Bing 了一番之后发现,Dockerfile 里面居然可以直接定义 Linux 安装包的命令,把依赖性搞定。直接加入一条RUN命令的步骤即可:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base
RUN apt-get update && apt-get install -y libgdiplus
WORKDIR /app
EXPOSE 80
EXPOSE 443
...
配置默认值
使用 Docker 容器部署应用的体验我希望是一键部署以后啥都不用干,直接能跑。而以前版本的博客系统,必须要求用户先手工配置一堆环境变量或是配置文件才能跑,非常996。
这个问题非常好办,只要在 appsettings.json 中留配置的默认值,保证程序能先跑起来即可。至于自定义的配置,可以让用户通过环境变量传给 Docker 容器。即保证了一键部署的方便性,又保留了自定义配置的灵活性。
小结
让 .NET Core 程序支持 Docker 并不麻烦。麻烦的是老一代 .NET 程序员会被根深蒂固的 Windows 设计所牵绊。在新的时代,我们必须学习新的实践,不要想着吃老本。.NET Framework 已经日薄西山,及时删库跑路,上 .NET Core + Docker 的船,才能保证在新的时代还能继续用 C# 释放生产力!我的 Docker 之旅刚刚起步,肯定还有很多我没遇到过的情况。欢迎读者在留言中补充和建议!