前言
有没有试问过自己,你的 docker 镜像是不是尽可能的小?有没有优化空间?为什么需要更小的镜像?
首先较大的镜像意味着如下几点:
- 更长的下载时间
- 更多的存储空间
- 更多的冗余组件
所有我们尝试下常规项目中是否可以优化我们的镜像大小
创建项目
开始前我们需要一个示例项目,这里就用dotnet new webapi -o Test
创建一个 net api 项目,的如下文件
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 2022-04-01 8:53 Controllers
d---- 2022-04-01 8:53 obj
d---- 2022-04-01 8:53 Properties
-a--- 2022-04-01 8:53 127 appsettings.Development.json
-a--- 2022-04-01 8:53 151 appsettings.json
发布项目
在终端执行
dotnet publish -c Release -o ./publish
创建 docker 镜像
使用 ASP.NET Core Runtime 创建一个 docker 镜像,Dockerfile 文件内容如下
FROM mcr.microsoft.com/dotnet/aspnet:6.0
EXPOSE 80 443
WORKDIR /app
COPY publish .
ENTRYPOINT ["dotnet", "Test.dll"]
构建镜像
docker build --pull --rm -f "Dockerfile" -t test:latest "."
如想查看应用是否可以正常运行,可尝试运行docker run -p 8080:80 test
命令创建容器,并访问http://localhost:8080/Weather
即可看到 json 数据
[
{
"date": "2022-04-02T01:20:36.7772078+00:00",
"temperatureC": -14,
"temperatureF": 7,
"summary": "Cool"
}
//....
]
查看镜像
执行docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test latest 84c40efd5e10 6 minutes ago 212MB
212MB,一个简单的应用构建的镜像高达 212MB 是不是有点太大了
让我们开查看下是什么让这个镜像如此之大:
docker history test
IMAGE CREATED CREATED BY SIZE COMMENT
84c40efd5e10 10 minutes ago ENTRYPOINT ["dotnet" "Test.dll"] 0B buildkit.dockerfile.v0
<missing> 10 minutes ago COPY publish . # buildkit 4.18MB buildkit.dockerfile.v0
<missing> 10 minutes ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 10 minutes ago EXPOSE map[443/tcp:{} 80/tcp:{}] 0B buildkit.dockerfile.v0
<missing> 2 days ago /bin/sh -c #(nop) COPY dir:d0b9b7817ce7b36ce… 20.3MB
<missing> 2 days ago /bin/sh -c #(nop) ENV ASPNET_VERSION=6.0.3 … 0B
<missing> 2 days ago /bin/sh -c ln -s /usr/share/dotnet/dotnet /u… 24B
<missing> 2 days ago /bin/sh -c #(nop) COPY dir:7ed68022dc665c2bf… 70.6MB
<missing> 2 days ago /bin/sh -c #(nop) ENV DOTNET_VERSION=6.0.3 0B
<missing> 2 days ago /bin/sh -c #(nop) ENV ASPNETCORE_URLS=http:… 0B
<missing> 2 days ago /bin/sh -c apt-get update && apt-get ins… 36.2MB
<missing> 3 days ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 3 days ago /bin/sh -c #(nop) ADD file:966d3669b40f5fbae… 80.4MB
基础镜像和 Linux 库占用 116M,Net 占用 90.9MB,App 占用 4.18MB
镜像优化
从上面的步骤我们得知仅仅一个 4MB 的应用,构建出来的镜像竟然高达 200+MB,现在我们正式尝试将镜像优化到尽可能的小
自包含发布
通过自包含部署 net 应用,可以将程序、第三方库、以及运行时一起发布,这样就可以选择更小的基础镜像构建
dotnet publish --runtime alpine-x64 -c Release --self-contained true -o ./publish
构建镜像
基础镜像有很多,根据你的需求选择不同的镜像,这里我选择 Alpine Linux
Alpine Linux 是一个独立的、非商业的、通用的 Linux 发行版,专为重视安全性、简单性和资源效率的高级用户而设计。
修改 Dockerfile,并执行构建命令docker build --pull --rm -f "Dockerfile" -t test:latest "."
FROM alpine:3.15.3
RUN apk add --no-cache libstdc++ libintl
EXPOSE 80 443
WORKDIR /app
COPY publish .
ENTRYPOINT ["./Test"]
此时再次执行 docker images 查看镜像大小
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test latest 30daa834b245 3 seconds ago 104MB
可以看到现在只有 104MB
验证镜像是否可执行docker run -p 8080:80 test
查看程序是否可以正常运行
不幸的是得到了如下错误
Error loading shared library libstdc++.so.6: No such file or directory (needed by ./Test)
Error loading shared library libgcc_s.so.1: No such file or directory (needed by ./Test)
Error relocating ./Test: _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE7reserveEm: symbol not found
Error relocating ./Test: _ZNKSt5ctypeIcE13_M_widen_initEv: symbol not found
Error relocating ./Test: _ZNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEE14_M_replace_auxEmmmc: symbol not found
Error relocating ./Test: _ZdlPv: symbol not found
...篇幅原因,省去多余日志
根据错误日志得知缺少 libstdc++
和 libintl
,我们在镜像里安装对应的依赖,因此修改 Dockerfile 内容如下,并再次执行docker run -p 8080:80 test
查看程序是否可以正常运行
FROM alpine:3.15.3
RUN apk add --no-cache libstdc++ libintl
EXPOSE 80 443
WORKDIR /app
COPY publish .
ENTRYPOINT ["./Test"]
依然不幸的得到如下异常
Process terminated. Couldn't find a valid ICU package installed on the system. Please install libicu using your package manager and try again. Alternatively you can set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. Please see https://aka.ms/dotnet-missing-libicu for more information.
at System.Environment.FailFast(System.String)
at System.Globalization.GlobalizationMode+Settings..cctor()
at System.Globalization.CultureData.CreateCultureWithInvariantData()
at System.Globalization.CultureData.get_Invariant()
at System.Globalization.CultureInfo..cctor()
at System.Globalization.CultureInfo.get_CachedCulturesByName()
at System.Globalization.CultureInfo.GetCultureInfo(System.String)
at System.Reflection.RuntimeAssembly.GetLocale()
at System.Reflection.RuntimeAssembly.GetName(Boolean)
at System.Reflection.Assembly.GetName()
at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.Configure(System.Action`2<Microsoft.AspNetCore.Hosting.WebHostBuilderContext,Microsoft.AspNetCore.Builder.IApplicationBuilder>)
at Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions.Configure(Microsoft.AspNetCore.Hosting.IWebHostBuilder, System.Action`2<Microsoft.AspNetCore.Hosting.WebHostBuilderContext,Microsoft.AspNetCore.Builder.IApplicationBuilder>)
at Microsoft.AspNetCore.Builder.WebApplicationBuilder+<>c__DisplayClass6_0.<.ctor>b__1(Microsoft.AspNetCore.Hosting.IWebHostBuilder)
at Microsoft.Extensions.Hosting.GenericHostBuilderExtensions+<>c__DisplayClass0_0.<ConfigureWebHostDefaults>b__0(Microsoft.AspNetCore.Hosting.IWebHostBuilder)
at Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions.ConfigureWebHost(Microsoft.Extensions.Hosting.IHostBuilder, System.Action`1<Microsoft.AspNetCore.Hosting.IWebHostBuilder>, System.Action`1<Microsoft.Extensions.Hosting.WebHostBuilderOptions>)
at Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions.ConfigureWebHost(Microsoft.Extensions.Hosting.IHostBuilder, System.Action`1<Microsoft.AspNetCore.Hosting.IWebHostBuilder>)
at Microsoft.Extensions.Hosting.GenericHostBuilderExtensions.ConfigureWebHostDefaults(Microsoft.Extensions.Hosting.IHostBuilder, System.Action`1<Microsoft.AspNetCore.Hosting.IWebHostBuilder>)
at Microsoft.AspNetCore.Builder.WebApplicationBuilder..ctor(Microsoft.AspNetCore.Builder.WebApplicationOptions, System.Action`1<Microsoft.Extensions.Hosting.IHostBuilder>)
at Microsoft.AspNetCore.Builder.WebApplication.CreateBuilder(System.String[])
at Program.<Main>$(System.String[])
看日志需要icu
,该问题有两个解决方法
-
dockerfile 中将
icu
和libstdc++
libintl
一起安装FROM alpine:3.15.3 #由于icu官方源在国内下载很慢,所以使用国内代替 RUN echo "http://mirrors.aliyun.com/alpine/v3.15/main/" > /etc/apk/repositories && apk add --no-cache libstdc++ libintl icu EXPOSE 80 443 WORKDIR /app COPY publish . ENTRYPOINT ["./Test"]
-
设置环境变量
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
(也可以在 dockerfile 中设置环境变量)FROM alpine:3.15.3 RUN apk add --no-cache libstdc++ libintl EXPOSE 80 443 WORKDIR /app COPY publish . ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 ENTRYPOINT ["./Test"]
这里为了方便我就直接在 docker 命令里加环境变量的方式
docker run -e DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 -p 8080:80 test
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app/
再次优化
自 net6 开始,发布的时候可以进行裁剪,就是将不必要的依赖不进行打包发布,但可能回导致一些问题,发布时使用PublishTrimmed
dotnet publish --runtime alpine-x64 -c Release -p:PublishTrimmed=true -o ./publish
此处未使用–self-contained true 是因为这个默认值为 true,详见官方文档
再次构建镜像并查看镜像大小
REPOSITORY TAG IMAGE ID CREATED SIZE
test latest 344730e17472 36 minutes ago 56.8MB
这次得到了仅 56.8MB 的镜像
我已经迫不及待的想验证下是否可以正常运行
执行docker run -e DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 -p 8080:80 test
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /app/
一切正常!很棒~
但此时如果代码中有时区相关的代码,如获取当前时间,你会发现时间由于缺少时区导致时间不准
这是因为 alpine 中没有时区相关库,需要修改 dockerfile 内容即可
FROM alpine:3.15.3
ENV TZ=Asia/Shanghai DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1
RUN echo "http://mirrors.aliyun.com/alpine/v3.15/main/" > /etc/apk/repositories \
&& apk add --no-cache libstdc++ libintl tzdata zeromq \
&& ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
&& echo $TZ > /etc/timezone
EXPOSE 80 443
WORKDIR /app
COPY publish .
ENTRYPOINT ["./Test"]
我们回顾下我们做了什么
- 尽可能的减少程序自身大小
- 使用自包含的方式发布
- 使用 net6 自带的裁剪功能
- 尽可能的减少基础镜像大小
- 使用不包含 net 运行时的基础镜像
- 使用更小的 alpine 作为基础镜像