本文以 WOL 的.NET 项目为例,介绍了 Dockerfile 的基础知识和编写要点,旨在帮助读者更好地理解和掌握如何为 .NET 应用创建和优化 Dockerfile。
1. 背景
前面我们已经勾选了 Docker 容器化支持,项目已经生成了一个默认的 Dockerfile。但在实际项目中,我们需要根据项目的实际需求和环境来定制化 Dockerfile,以便更好地利用 Docker 的优势。本文将以 WOL (Wake On LAN) 项目为例,详细介绍如何编写一个针对 .NET 应用的 Dockerfile 文件,并分享一些实用的编写技巧。
2. 从默认模板入手
2.1 默认模板
以下是 .NET8 中 Web API 项目的默认模板,在这个默认模板中,我们可以看到一个典型的多阶段构建过程。
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
USER app
WORKDIR /app
EXPOSE 8080
EXPOSE 8081
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WebApplication3/WebApplication3.csproj", "WebApplication3/"]
RUN dotnet restore "./WebApplication3/./WebApplication3.csproj"
COPY . .
WORKDIR "/src/WebApplication3"
RUN dotnet build "./WebApplication3.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./WebApplication3.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WebApplication3.dll"]
2.2 模板介绍
根据默认模板,我们先来复习一下 Dockerfile 的基本结构:
- FROM:指定基础镜像,并可以使用
AS
为这个阶段设置别名,如FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
- USER:设置运行时的用户,如
USER app
- WORKDIR:设置工作目录,如
WORKDIR /app
- EXPOSE:暴露容器需要监听的端口,如
EXPOSE 8080
和EXPOSE 8081
- COPY:复制文件或目录,如
COPY ["WebApplication3/WebApplication3.csproj", "WebApplication3/"]
- RUN:执行命令,如
RUN dotnet restore "./WebApplication3/./WebApplication3.csproj"
- ARG:定义构建参数,如
ARG BUILD_CONFIGURATION=Release
- ENTRYPOINT:指定容器启动时执行的命令,如
ENTRYPOINT ["dotnet", "WebApplication3.dll"]
在默认模板中,我们还可以看到一个典型的多阶段构建过程。多阶段构建可以减小最终生成的镜像大小,提高构建速度。在上述示例中,我们使用了四个阶段,分别命名为 base、build、publish 和 final,用于不同的构建和运行阶段。
3. WOL 项目的 Dockerfile
一般来说使用默认的 Dockerfile 就可以了,但是有时候我们需要根据项目的特定需求和环境对 Dockerfile 进行调整和优化。
3.1 额外的依赖
项目可能需要一些额外的系统依赖或者工具。可以通过 RUN 指令和包管理器(如 apt-get、yum 等)来安装这些依赖。有时我们可以在编程中就能发现并意识到,但是更多的时候需要我们在容器化后对项目做好测试工作,确保所有功能都能正常运行。
比如在 WOL 项目的工具类中有写这样一个检查设备是否在线的函数:
internal static bool Ping(string iP)
{
// 检查IP是否在线
Ping ping = new Ping();
PingReply pingReply = ping.Send(iP,100);
return pingReply.Status == IPStatus.Success;
}
写起来倒是很简单,也很省事。但是如果不知道其运行原理的话,就会在容器化后遇到问题。这个函数使用了 System.Net.NetworkInformation.Ping
类来检查目标 IP 地址是否在线,但是其依然是使用了系统的 Ping
工具来处理。Docker 的 Base 镜像都是最简版本,为了轻便,都是不包含 Ping 工具的。
为了解决这个问题,我们就需要将缺失的依赖安装恢复,Debian 的 Ping 工具包为 Iputils-ping
,在 final
阶段通过 apt 安装即可。
3.2 更改安装源
在上一节中,我们讨论了安装额外依赖的必要性。为了加速镜像构建过程中的软件包安装,我们可以考虑修改安装源。通常,我们会在物理机上更改镜像源以加速软件包的安装。同样的方法也适用于容器镜像的构建过程。如果不更改安装源,一个镜像的构建可能会花费数小时,这对于开发者来说是难以忍受的。
在使用 mcr.microsoft.com/dotnet/aspnet:8.0
镜像时,底层基于 Debian 系统。为了加速软件包的安装,我们可以更改安装源。在这个例子中,我们将安装源更改为清华大学的镜像源。以下是如何在 Dockerfile 中进行更改的示例:
sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources && \
apt-get update
这段代码使用 sed
命令修改了 /etc/apt/sources.list.d/debian.sources
文件中的安装源,将其从 deb.debian.org
更改为了 mirrors.tuna.tsinghua.edu.cn
。接下来,使用 apt-get update
命令更新软件包列表。
通过这样的设置,我们可以加速后续软件包的下载和安装过程,从而提高整体镜像构建速度。
3.3 清理垃圾
在执行 apt update
和安装软件包之后,我们需要注意清理系统中产生的缓存和临时文件。这些文件会增加镜像的大小,而在大多数情况下,它们在运行时并不需要。因此,我们可以在 Dockerfile 中添加相应的指令来清理这些垃圾文件,从而减小最终生成的镜像大小。
在使用 apt-get
安装软件包后,可以使用以下命令清理缓存:
apt-get clean
rm -rf /var/lib/apt/lists/*
这段代码首先使用 apt-get clean
命令清理软件包缓存,然后使用 rm -rf /var/lib/apt/lists/*
删除下载的软件包列表。这样一来,我们就可以在构建过程中有效地减小镜像大小。
3.4 权限问题
前面已节我们已经按照了系统的 ping 工具,这样我们就可以正常的使用 System.Net.NetworkInformation.Ping
类来检查目标 IP 地址是否在线。但是在 Linux 系统中,Ping 的实现依赖于 ICMP 协议,而在容器中执行 ICMP 请求通常需要特殊权限,直接容器化运行项目会发现下面的问题:
ping: socktype: SOCK_RAW
ping: socket: Operation not permitted
ping: => missing cap_net_raw+p capability or setuid?
这是因为为了提高安全性,我们已经将应用的运行用户更改为了 app。为了解决这个问题,我们可以在 Dockerfile 中为更改 ping 命令的权限。
chmod u+s /bin/ping
3.5 编码问题
处理到这里,Dockerfile 的文件就基本解决完毕。但是在实际测试中我们会发现,API 接口中返回的中文会出现乱码的情况:
我们可以看到,由配置文件返回的中文信息并没有出现乱码,而通过代码直接返回的中文出现了乱码的情况。这里是因为默认的代码文件编码是 GB18030 ,我们只需要将其改为 UTF-8 ,然后重新制备镜像即可解决这种硬编码导致的乱码问题。
4. 要点总结
通过以上的问题重现和处理,我们最终的项目 Dockerfile 如下:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["WakeOnLan/WakeOnLan.csproj", "WakeOnLan/"]
RUN dotnet restore "WakeOnLan/WakeOnLan.csproj"
COPY . .
WORKDIR "/src/WakeOnLan"
RUN dotnet build "WakeOnLan.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "WakeOnLan.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final
RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
&& apt-get update \
&& apt-get install -y --no-install-recommends iputils-ping \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* \
&& chmod u+s /bin/ping
WORKDIR /app
USER app
EXPOSE 8080
COPY --from=publish /app/publish .
ENTRYPOINT ["./WakeOnLan"]
最后总结在编写 Dockerfile 时需要注意的一些要点:
-
使用多阶段构建:多阶段构建可以减小最终生成的镜像大小,提高构建速度。在上述示例中,我们使用了三个阶段,分别用于编译、发布和运行应用程序。
-
使用官方镜像:尽量使用官方提供的基础镜像,以确保镜像的安全性和可靠性。
-
优化镜像层:尽量将不会经常变动的文件放在前面,以充分利用镜像层缓存,提高构建速度。
-
安装额外依赖:根据项目实际需求,安装额外的依赖和工具,确保所有功能正常运行。
-
更改安装源:更改镜像源以加速软件包的下载和安装过程,减少构建时间。
-
清理缓存和临时文件:清理缓存和临时文件,减小生成的镜像大小。
-
解决权限问题:确保容器化应用程序的安全性,遵循最小权限原则,避免使用 root 用户运行容器。设置运行时用户并调整文件和目录的权限。
-
充分测试:对容器化后的项目进行充分的测试工作,以确保所有功能都能正常运行。
通过关注这些要点,我们可以针对实际项目编写和调整 Dockerfile,以便更好地利用 Docker 的优势。同时,对容器化后的项目进行充分的测试工作也是至关重要的。