縮減Docker鏡像體積歷程總結

容器化的過程當中老是免不了要構建鏡像,一個體積更小的鏡像除了可以節省機器的磁盤空間以外,還可以提高傳輸效率。這篇文章主要是想講述一下本身在優化鏡像體積時所採起的措施,固然並非全部方案都對減小鏡像體積有明顯效果,具體項目還要具體分析。這篇文章我以Rails項目的鏡像構建做爲例子。node

爲何構建出來的鏡像這麼大?

在優化鏡像大小以前首先要知道爲什麼咱們所構建的鏡像會這麼大?下面是我項目中用於構建鏡像的Dockerfile文件mysql

FROM ruby:2.5.3

RUN apt-get update -y && apt-get install -y \
        build-essential \
	    imagemagick \
        default-libmysqlclient-dev
RUN apt-get install -y \
        nodejs \
        yarn
RUN rm -rf /var/lib/apt/lists/*

WORKDIR /beansmile-web
COPY . /beansmile-web

RUN bundle install
複製代碼

鏡像文件我定義得比較隨意,它所構建出的鏡像信息以下git

web1                 latest               1a8a32d5253a        9 hours ago         1.26GB
複製代碼

構建的鏡像的過程跟日常基於一個操做系統打造供項目運行基礎環境的過程差很少。只是平常的操做系統一般都不僅一個項目在運行,所以系統裏所包含的東西是比較全面的。而鏡像只指望提供給特定的項目使用,所以所依賴的東西比較有針對性,沒必要要的東西儘可能不要加進去。github

針對上面的Dockerfile文件我以爲有如下幾個優化方向web

  1. 基礎的ruby:2.5.3是基於buildpack-deps來構建的其中包含大量額外的軟件包,或許用更輕量級的鏡像來做爲基礎鏡像可以進一步縮減空間。
  2. 文件中有太多的RUN命令,每一條命令都會疊加層數,可能會形成體積的變大。
  3. 把整個項目目錄都拷貝到鏡像中,並非個好主意,隨着項目的增大,Rails項目中的public目錄下可能會存在着圖片之類的靜態資源,把這些東西打包到鏡像中意義不大。
  4. 是否可以採用multi-stage的構建方式,把一部分不那麼緊要的資源丟棄,讓最終鏡像體積更小?

下面一條條來分析。sql

具體分析

方向1: 基於更小的操做系統

前面的例子最終構建出來的鏡像體積十分龐大,主要歸咎於相關的基礎鏡像自己就很大。docker

REPOSITORY          TAG                  IMAGE ID            CREATED             SIZE
ruby                2.5.3                60c3a1518797        3 weeks ago         871MB
web1                latest               1a8a32d5253a        9 hours ago         1.26GB
複製代碼

可見咱們的基礎Ruby鏡像自己就800多M了,構建鏡像的過程還須要安裝依賴,致使了最終的web鏡像體積會達到1.26G。這個體積可不利於網絡傳輸,官方所提供的Ruby基礎鏡像有許多個版本,除了Ruby自己的版本不一樣以外,還有許多基於不一樣操做系統所構建的基礎鏡像能夠選擇,而這些不一樣的操做系統所構建出來的Ruby基礎鏡像的體積相差甚大ubuntu

REPOSITORY          TAG                  IMAGE ID            CREATED             SIZE
ruby                2.5.3-slim-stretch   20132a4ab93d        2 weeks ago         129MB
ruby                2.5.3                60c3a1518797        3 weeks ago         871MB
ruby                2.5.3-alpine         b3361f13ff1f        3 weeks ago         43.6MB
複製代碼

基於alpine操做系統的Ruby鏡像是最迷你的,只有43.6MB。slim-stretch也是個不錯的選擇。或許採用更輕量級的鏡像將會是一個優化的契機。緩存

經驗小貼士: 從我本身的構建經驗來看,採用slim-stretch或許會是更加親民的選擇,它是Debian系,包管理器跟ubuntu是同樣的都是用apt-get,用慣ubuntu的人確定會以爲比較親切。alpine所用的包管理器是apk(是否是想到安卓的安裝包?),一些經常使用包的命名有點不太同樣須要本身慢慢去解決。*ruby

不過不管用哪一種方案都避免不了時間的投入,網上也沒那麼多現成的解決方案,迷你鏡像的話你不得不本身安裝一些構建過程當中所依賴的軟件。

方向2: 縮減鏡像的層數

Docker官網對鏡像的說法是,它是由一層層的只讀層組成的,層次越少鏡像的性能表現越出衆。這也是官方建議咱們採用特定基礎鏡像去構建本身的項目鏡像,而不是基於一個赤裸裸的操做系統鏡像(如Ubuntu鏡像)的緣由。

上述的例子中咱們用了三個RUN命令,這會無心中多構建了兩個層,其實咱們能夠把它合併成一條RUN命令

RUN apt-get update -y && apt-get install -y \
        build-essential \
	    imagemagick \
        default-libmysqlclient-dev \
        nodejs \
        yarn \
        && rm -rf /var/lib/apt/lists/*
複製代碼

基於這個改動從新建立一個鏡像web2

REPOSITORY          TAG                  IMAGE ID            CREATED             SIZE
web2                latest               221a316a6903        14 minutes ago      1.25GB
web1                latest               1a8a32d5253a        9 hours ago         1.26GB
複製代碼

可見這種改動對於縮減鏡像體積效果並不明顯

官方的說法是這樣的

In older versions of Docker, it was important that you minimized the number of layers in your images to ensure they were performant.

咱們能夠得出結論,或許縮減層數主要是爲了讓鏡像操做起來更高效吧,減小層數這個優化方向對於縮減鏡像體積並無多大的幫助,不過咱們這樣作仍是有好處的。

方向3: 忽略一些文件

從上面的配置能夠看出,爲了方便鏡像的構建我直接把整個項目都移動到鏡像中去(COPY命令)。然而對於構建的鏡像而言,並非全部的文件咱們都應該關心,最爲值得關心的應該只有源碼部分。因此我預想着在構建的鏡像中能夠把如下的目錄剔除掉

  • public/: 用於存放一些靜態文件的目錄,若是其中包含大量像圖片這樣的資源的話會對鏡像的體積有較大的影響。
  • tmp/: 用於存放一些緩存資源,項目進程文件等等,這些文件對於鏡像而言用處不大。
  • log/: 用於存放日誌相關的信息。

PS: 固然每一個人對實際項目的考量會有所不一樣,這幾個目錄只是根據我我的的項目狀況所作的決定,並不具備通用性。

要忽略這些文件,咱們採用一個名爲.dockerignore的文件,把它放在當前的目錄下便可,它的寫法跟.gitignore文件很類似,內容大概以下

/public/**
/tmp/**
/log/**
複製代碼

而後從新構建鏡像

web3                latest               fb13cc1301b2        About a minute ago   1.2GB
web2                latest               221a316a6903        23 hours ago         1.25GB
web1                latest               1a8a32d5253a        33 hours ago         1.26GB
複製代碼

這種方式的影響也不怎麼大,這是由於目前我本地這些目錄下所包含的「垃圾」資源所佔的比重較小。

方向4: multi-stage方案

這個是官方推薦的方案,在Docker17.05以後能夠使用

In Docker 17.05 and higher, you can do multi-stage builds and only copy the artifacts you need into the final image. This allows you to include tools and debug information in your intermediate build stages without increasing the size of the final image.

好像看起來有點複雜,不過它的原理大概就是先使用一個體積較大,依賴較爲齊全的鏡像來構建所須要的資源,而後把這些資源複製到一個輕量的基礎鏡像中,並繼續咱們的鏡像構建工做,這樣就能夠把原先龐大的基礎鏡像給拋棄了。這種作法能避免咱們最終的鏡像中包含了一堆無用的依賴,在某種程度上可以減小最終鏡像的體積。

這看起來是個很不錯的策略,我也在項目中進行了嘗試。咱們決定把bundle依賴包的安裝以及,靜態文件的編譯都放到一個功能完備的基礎鏡像中去完成,而後把所須要的資源拷貝到一個輕量級的基礎鏡像中(相似alpine這種輕量級系統的相關鏡像)再繼續完成構建步驟。

不過我構建過程當中遇到以下問題

  • 用bundle安裝依賴的過程當中不只僅涉及到ruby代碼的引入,mysql2nokogiri這些第三方庫除了會引入Ruby代碼以外還會在安裝的時候進行編譯,並生成一些共享庫,若是把依賴資源從一個鏡像拷貝到另外一個鏡像的話除了要拷貝bundle相關目錄下的ruby代碼以外,還不得不拷貝這些第三方庫所依賴的共享庫,這比想象中要麻煩。
  • 咱們指望在一個鏡像裏面完成靜態文件的構建,那麼咱們即可以在最終鏡像中免去了安裝nodejs, yarn這些用於編譯靜態資源相關的依賴了。不事後來仍是以爲這種方案不太適用。一方面,安裝了nodejs與沒有安裝nodejs的鏡像差異也就是30M左右,另外一方面,要運行bin/rails c須要依賴JS運行時,這不管對於開發仍是生產都是一個比較重要的操做,所以在最終鏡像中捨棄JS運行時並非個好主意。

最終構建

前面提到了4個優化的方向,但彷佛最終只有

  • 採用更輕量級的操做系統的相關基礎鏡像來進行構建。
  • multi-stage。

對最終的鏡像體積影響較大。考慮到multi-stage的解決方案所帶來的好處可能還不如麻煩來得多,所以最終仍是捨棄了這個方案,與其這樣繞來繞去還不如直接採用最精簡的ruby:2.5.3-alpine做爲基礎鏡像來打造本身的項目鏡像。選擇一個精簡的操做系統最大的問題就是在構建項目鏡像過程當中的全部基礎依賴都得本身一個個去解決,要投入很多的時間和精力,如下是我通過反覆測試所獲得的Dockerfile文件(僅供參考,畢竟你的項目所依賴的東西可能有所不一樣)

FROM ruby:2.5.3-alpine

RUN apk --update --upgrade add \
        # bundle 安裝相關的依賴
        git \
        curl \
        # mysql2 依賴
        mysql-dev \
        # 基礎設施,好比gcc相關的東西
        build-base \
        # nokogiri 相關依賴
        libxslt-dev \
        libxml2-dev \
        # 圖片處理相關依賴
        imagemagick \
        # tz相關,若是沒有bundle的時候會報錯
        tzdata \
        nodejs \
        yarn \
        && rm -rf /var/cache/apk/*

WORKDIR /beansmile-web
COPY . /beansmile-web/
RUN bundle install
複製代碼

構建出來的鏡像以下

web4                latest               71b75128d0d9        14 hours ago         586MB
複製代碼

與以前的鏡像相比體積大幅度減小了。這是一個咱們能夠接受的大小了,考慮到時間成本就不進一步壓縮了。

總結

這篇文章主要簡單總結了我的在縮減Rails項目鏡像方面的探究。爲了縮減鏡像體積提出了4個主要的優化方向,用迷你的操做系統構建鏡像的方式來減小鏡像的體積的方式十分有效。不過不一樣類型,基於不一樣語言的項目可能會有不一樣的側重點,不能一律而論,可能有的項目中multi-stage會幫你省下更多的時間。

相關文章
相關標籤/搜索