net6 docker镜像大小优化之路

本文介绍了如何通过优化.NET应用程序的发布方式和选择更小的基础镜像来减小Docker镜像的大小。通过自包含发布、使用Alpine Linux、裁剪不必要的依赖以及设置环境变量,最终将一个212MB的镜像优化到了56.8MB。在优化过程中,还解决了因缺失库文件导致的运行时错误。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

有没有试问过自己,你的 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 中将iculibstdc++ 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 作为基础镜像
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值