.NET 6 的 docker 镜像可以有多小?
Intro
最近写了一个小玩具,一个命令行调用 HTTP API 的工具,介绍可以参考:动手造轮子 —— dotnet-HTTPie,
最近在使用 System.CommandLine
重构的同时,也在尝试减少 docker 镜像的大小,这样下载镜像也会比较快一些
Before
之前的做法是在 runtime 容器中安装一个 dotnet tool,然后镜像生成出来之后大概是 89.9M,
runtime 的镜像大小就已经有 87.3M,dockerfile如下:
FROM mcr.microsoft.com/dotnet/runtime:3.1-alpine AS base
LABEL Maintainer="WeihanLi"FROM mcr.microsoft.com/dotnet/sdk:3.1-alpine AS build-env
# dotnet-httpie version, docker build --build-arg TOOL_VERSION=0.1.0 -t weihanli/dotnet-httpie:0.1.0 .
ARG TOOL_VERSION
RUN dotnet tool install --global dotnet-httpie --version ${TOOL_VERSION}FROM base AS final
COPY --from=build-env /root/.dotnet/tools /root/.dotnet/tools
ENV PATH="/root/.dotnet/tools:${PATH}"
最初是基于 dotnetcore 3.1 的,所以用的是 3.1 的镜像,后面更新到了 6.0,虽然会比 3.1 小一点点,但还是会有 80 多 M,.NET 6.0 runtime 的镜像有 81.4 M,而一个 nginx 只有 22.8 M,Redis 也只有 32.3M,还是蛮大的
Now
如果有注意 dotnet 镜像的 tag 的话,你会发现有一类是 runtime-deps
,就是运行时必不可少的依赖,但是里面是不包含 sdk 和 runtime 的,这通常用于部署 self-contained 的应用,就是自己包含了运行时所需的所有依赖,拉了一个 .NET 6.0 runtime-deps
的镜像,大小只有 11.9 M,仿佛看到了希望,也许能够和 nginx 以及 redis 相媲美了
对于发布 self-contained
应用只需要在发布时指定 --self-contained
并且要指定一个 runtime 信息,官方叫法是 RID(Runtime Identifier),就是要发布平台的环境信息。
可以使用下面的命令来发布一个 self-contained 应用,因为想作为一个 dotnet-tool 一样去使用,所以我们指定了 PublishSingleFile
来生成一个单文件应用,另外为了使用指定的 command 我们也指定了 AssemblyName
为我们实际想要使用的 command http
dotnet publish ./HTTPie/HTTPie.csproj -c Release --self-contained --use-current-runtime -p:AssemblyName=http -p:PublishSingleFile=true
来看一下这样 build 出来的镜像大小吧,这里我们就不再是安装 dotnet tool 了,而是直接对源码进行 build && publish,docker 镜像如下:
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine AS base
LABEL Maintainer="WeihanLi"FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build-envWORKDIR /app
COPY ./src ./
COPY ./build/version.props ./Directory.Build.props
RUN dotnet publish ./HTTPie/HTTPie.csproj -c Release --self-contained --use-current-runtime -p:AssemblyName=http -p:PublishSingleFile=true -o ./artifactsFROM base AS final
COPY --from=build-env /app/artifacts /root/.dotnet/tools
ENV PATH="/root/.dotnet/tools:${PATH}"
这里使用发布单文件应用来代替了原来安装 dotnet-tool 的过程,这样打包出来的镜像 77.7 M 比原来小了一些
从 .NET Core 3.0 开始,微软提供了一个 Trim 选项,可以移除引用的程序集里可能用不到的代码,我们来尝试一下,指定 PublishTrimmed
来试一下,使用下面的命令来进行发布
dotnet publish ./HTTPie/HTTPie.csproj -c Release --self-contained --use-current-runtime -p:AssemblyName=http -p:PublishSingleFile=true -p:PublishTrimmed=true
再来看一下打包出来的镜像大小,此时已经变成了 33.4 M
已经变之前小了一半,和 redis 已经差不多了,还能不能够更小呢?
指定 Trim 的时候会有很多警告,这是因为有些方法可能会用反射来使用某些代码,并没有直接的依赖关系,此时这种方式就有可能会造成一些问题,所以使用 Trim 的时候如果程序比较复杂需要好好的测试一下以确保没有问题
.NET 6 在 Preview 4 的时候引入了一个新的功能 .NET 6 Preview 4 Released,针对单文件应用的发布提供了一个压缩选项,我们可以通过指定 EnableCompressionInSingleFile
来进一步对单文件应用进行压缩,从而进一步减小文件的大小
dotnet publish ./HTTPie/HTTPie.csproj -c Release --self-contained --use-current-runtime -p:AssemblyName=http -p:PublishSingleFile=true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true
我们再来看一下现在构建出来的镜像大小,现在我们的镜像已经减小到了 26.9M,已经比 redis 小了
这样基本就达到了我们的预期,是不是还有优化的空间呢
我们通过 dive 来看一下镜像里的内容,在最后的拷贝的时候,可以看到我们拷贝过去的其实有两个文件一个是 http
,另一个是 http.pdb
,pdb
文件其实是不需要的,所以我们在拷贝的时候可以只拷贝 http
就可以了
但是 pdb
文件很小,只有几十k,所以去掉了以后打包还是有 26.9M,但是镜像里就只有一个文件了,就很舒适
dive 是一个非常有帮助的工具来查看 docker 镜像里每一层的内容,在镜像启动不起来,镜像有问题的时候是一个非常好的分析工具
完整的 Dockerfile 如下:
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0-alpine AS base
LABEL Maintainer="WeihanLi"FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build-envWORKDIR /app
COPY ./src ./
COPY ./build/version.props ./Directory.Build.props
RUN dotnet publish ./HTTPie/HTTPie.csproj -c Release --self-contained --use-current-runtime -p:AssemblyName=http -p:PublishSingleFile=true -p:PublishTrimmed=true -p:EnableCompressionInSingleFile=true -o ./artifactsFROM base AS final
COPY --from=build-env /app/artifacts/http /root/.dotnet/tools/http
ENV PATH="/root/.dotnet/tools:${PATH}"
最后对我们的 docker 镜像进行测试
使用类似的方法对一个 hello world 应用测试一下, hello-world 是一个 console,23.4M ,API 是一个 asp.net core web api 应用,34.3M
上传到 dockerhub 之后,看到的大小会更小一些,docker registry 会对镜像进行压缩
More
使用 dotnet publish 而不是使用 dotnet tool 的方式除了大小之外,还有一些别的好处,现在我们发布包到 nuget 的时候往往会有一定的时间才能获取到这个包,现在更新 docker 镜像都是手动去做的,因为要等 nuget 上出现这个版本的包以后再进行打包,就不够自动化,使用 publish 的方式可以更好的自动化地发布
.NET 6 SDK 后续会针对 self-contained
进行一些优化,对于 --self-contained
可以使用 --sc
来代替,一个别名,简化使用,同时使用 --self-contained
的时候会默认自动使用当前 SDK 的 RID,这样发布 self-contained 应用就会更加方便了,详细可以参考 issue:https://github.com/dotnet/sdk/issues/19576
References
https://hub.docker.com/_/microsoft-dotnet-runtime-deps/
https://hub.docker.com/repository/registry-1.docker.io/weihanli/dotnet-httpie/tags?page=1&ordering=last_updated
https://github.com/WeihanLi/dotnet-httpie
https://docs.microsoft.com/en-us/dotnet/core/deploying/trim-self-contained?WT.mc_id=DT-MVP-5004222
https://devblogs.microsoft.com/dotnet/announcing-net-6-preview-4/#compression
https://github.com/wagoodman/dive
.NET 6 Preview 4 Released
动手造轮子 —— dotnet-HTTPie