"Moonglade", the open-source blog system used by my blog (https://edi.wang), has been around for over a year. At least four community friends have used the system to deploy their own blogs on Azure and Alibaba Cloud. Unfortunately, the system has long lacked Docker support, which is the political correctness of today's world. Recently things have changed and I successfully made my blog system run on Docker now.

My Docker Environment

Actually I didn't install Docker on my development machine for some reason. The whole Docker environment is deployed on Azure, with Azure DevOps + Docker Hub + Azure App Service Linux Plan. When I submit my code to GitHub, Azure DevOps will kick in, build the Docker image and push to Docker Hub. Then it can be tested with an Azure App Service running on Linux Plan.

Dockerfile

Visual Studio can add Docker support to an ASP.NET Core project just by a few clicks. But it not only added a Dockerfile into your project directory but also modify your project file (.csproj), which is to associate your project with some Docker tooling support for local debugging. Actually, the minimal setup for an ASP.NET Core project to use Docker is just a Dockerfile, nothing more. 

At first, my Dockerfile was this:

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 rest project files ...

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"]

It's just like what Visual Studio would auto-generate for a common ASP.NET Core project. I modified the build and publish parameter to include a specific version string with "-docker" so that when the system is deployed, I can easily tell which instance of the blog is running on Docker from the web page UI.

YAML

I compile and deploy my blog project using YAML on Azure DevOps, where the Docker step is defined as follows:

- job: Docker
  pool:
    vmImage: 'ubuntu-latest'
  steps:
  - task: Docker@2
    displayName: 'Build and Push'
    inputs:
      containerRegistry: 'ediwang_dockerhub'
      repository: ediwang/moonglade
      tags: latest

Because the VM image is Ubuntu, this Docker image is compiled into the Linux/x64 architecture. If you need a different schema, you can add other types of VM images yourself.

ediwang_dockerhub is the connection name for Docker Hub that was pre-configured in Azure DevOps. Once the compilation is complete, Azure DevOps uses its authorization to publish an image to Docker Hub.

Issue #1: Path

When I run my blog Docker image for the first time, I noticed that the blogger Avatar, RSS/ATOM, OPML files were all 404. I quickly realized that this is the path issue with Linux. 

In Windows systems, the path that represents a file or folder is typically split by a backslash, such as:

C:\Fubao\996.icu

In Linux systems, the path has to be split by slashes, such as:

/use/dotnet/work/955

Many old .NET programmers like me, would just write path as Windows-style because .NET was intended for Windows users only. An example in my blog code:

var fallbackImageFile = $@"{AppDomain.CurrentDomain.GetData(Constants.AppBaseDirectory)}\wwwroot\images\default-avatar.png";

Actually, .NET has an API to help us dealing with the path in different systems. Path.Combine()which would use the correct type of slash for the target system. So, you may do the same change as me:

var cssPath = Path.Combine(webRootPath, "css", "theme", currentTheme);

Would this really work for every scenario?

Nope, for example:

var p1 = "/dotnet";
var p2 = "/fubao/996.icu";
Path.Combine(p1, p2);

Guess what the output is?

The "/dotnet" was eaten. This can be a problem for the Linux platform.

Luckily, Microsoft introduced a new API "Path.Join()" in .NET Standard 2.1, it can output the desired path:

So, I made a lot of changes around my blog code to use Path.Join(), and finally made resources work again.

Issue #2: libgdiplus

When running the blog container, there was another issue, log as follows:

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 ---

It's because my blog code used some image types that requires a library named libgdiplus on Linux. To fix this, add one more step into the Dockerfile to install libgdiplus during build process.

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
...

You may have to use other package managers rather than apt if your build system isn't Ubuntu.

Default Settings

To make the Docker version of my blog deployment as easy as possible, I want it to be ready to use out of the box. Previous versions of the blog require manually setup environment variables or edit appsettings.json. This is no longer the case. I updated the default appsettings.json to include default values that ensure the blog system can run out of the box. Users can still setup environment variables if they want to customize pre-defined settings. That is, the convenience of one-click deployment is guaranteed, while the flexibility of custom configuration is preserved.

Closing

To add Docker support for a .NET Core application wasn't very hard. The point is we old .NET programmers must not be bound to our Windows style mindset. This is a new era for .NET, we must keep learning modern practices so that we can still use our favorite .NET technology to empower our customers in today's world.