Dockerfile 慣用法,應該分發更小的容器

hedzr發表於2019-09-18

Dockerfile Idioms

分發更小的容器,是一種應該被推薦的行為。

更小的容器,啟動會更快,分發會更快,重用會更快,……。對於生命來說,快是一種態度,代表你的時間被消費的更有價值:節約時間總是正確的。

幾年前,其實我就想寫有關容器縮減的內容,不過那時候 alpine 還不被重視,它自己也很難用,所以我們更多地是在 ubuntu 這樣的大塊頭上研究怎麼削減最終尺寸。

值得欣慰的是,那些曾經用到的原則一直都是對的,儘管這個世界、這些生態在變,但準則沒有變。

現在,關於容器尺寸削減的問題,文章多得很,問答也很多。

但我還是打算寫一篇。寫者嘛,總是覺得自己寫的內容對一點,邏輯對一點,用詞對一點,覆蓋面對一點,限定語對一點。等等。

不過,這次準備隨便寫,不打算整構邏輯了。

Checklist

削減容器的最終尺寸,首先考慮如下的 Checklist:

  • 採用更小的基準包

    • 在絕大多數情況下,alpine是最佳選擇
    • 極端情況下可以使用 distroless,但後果是沒有shell,無法進入容器
    • 有的時候你可以使用 busybox
  • 選取最恰當的基準包。

    • 對於 golang 服務來說,alpine:latest 是最佳選擇
      • 但如果你需要golang構建操作,則 golang:alpine 可能才是對的
    • 對於 java 系列來說,這些都是好選擇:
    • 對於 nodejs 來說,alpine 版本是最佳的:node:8-alpine
    • 等等,沒法一一按語言列舉。
  • 使用包安裝命令時,記住清除包安裝過程所下載的索引、安裝包

    後面我會在慣用法中更多介紹這一點。

  • 去掉記憶體交換機制,去掉交換分割槽

  • 不要安裝帶有 ncurse 依賴的工具,例如 mc

  • 不要安裝帶有除錯工具或者除錯工具性質的工具,例如 vim,curl。一定要用,使用 nano 和 wget 替代它們

  • 調整命令順序,合併相同命令,使得產生更少的層

  • 使用記憶功能以便去掉打包過程中才會使用的包,從而縮減最終容器尺寸

    這個記憶功能,主要是指 alpine apk --virtual 功能;對於 apt 則有一個 apt-mark 工具。

  • 對於 apt 使用 apt install --no-install-recommends -y 方式

  • 使用多遍構建過程,將打包和中間內容排除在最終容器之外,以縮減其尺寸

下面都是基於 voxr vdeps-base 來介紹。

可以查閱 github.com/hedzr/docke…

各種慣用法

alpine apk 的慣用法

較典型的做法是這樣子:

RUN fetchDeps=" \
    		ca-certificates \
    		bash less nano iputils bind-tools busybox-extras \
    		wget lsof unzip \
    	"; \
    apk update \
    && apk --update add ${fetchDeps} \
    && apk info -vv | sort \
    && apk -v cache clean && rm /var/cache/apk/*
# 摘自 https://github.com/hedzr/docker-basics/blob/master/alpine-base/Dockerfile
複製程式碼

apk -v cache cleanrm /var/cache/apk/* 兩者選一就可以了,這裡只是為了示例。

比上例更嚴格精確、也更節省空間的辦法是:

RUN build-deps="gcc
      freetype-dev \
      musl-dev \
      "; \
    apl add --update --no-cache bash less nano unzip && \
    apk add --no-cache --virtual .build-deps ${buildDeps} && \
    pip install --no-cache-dir requests && \
    apk del .build-deps && \
    rm /var/cache/apk/*
複製程式碼

採用國內映象伺服器加速,更舒適的結構:

# 改編自 hedzr/docker-basics/golang-builder
RUN fetchDeps=" \
              ca-certificates \
            "; \
    buildDeps=" tig "; \
       cp /etc/apk/repositories /etc/apk/repositories.bak; \
       echo "http://mirrors.aliyun.com/alpine/v3.10/main/" > /etc/apk/repositories; \
       apk update \
    && apk add --virtual .build-deps ${buildDeps} \
    && apk add ${fetchDeps} \
    && echo
    && echo "Put your building scripts HERE" \
    && apk del .build-deps \
    && rm /var/cache/apk/* \
複製程式碼

debian apt 的慣用法

RUN fetchDeps=" \
    	   ca-certificates \
    	   wget nano vim.tiny net-tools iputils-ping lsof \
    	   dnsutils inetutils-telnet locales \
    	 "; \
       TZ=Etc/UTC; LOCALE=en_US.UTF-8; \
       apt update \
    && DEBIAN_FRONTEND=noninteractive apt install -y --no-install-recommends ${fetchDeps} \
    && locale-gen $LOCALE \
    && cat /etc/default/locale && echo "Original TimeZone is: $(locale -a)" && date +'%z' \
    && ln -s /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ | tee /etc/timezone \
    && echo "Current TimeZone updated: $(locale -a)" && date +'%z' \
    # && apt-get purge -y --auto-remove ${fetchDeps} \
    && rm -rf /var/lib/apt/lists/*
# 時鐘時區部分可以去掉
# 摘自 https://github.com/hedzr/docker-basics/blob/master/ubuntu-mod/Dockerfile
複製程式碼

對於用到 python pip 的場景還可以這樣:

RUN buildDeps="curl python-pip" && \
    apt-get update && \
    apt-get install -y --no-install-recommends $buildDeps && \
    pip install requests && \
    apt-get purge -y --auto-remove $buildDeps && \
    rm -rf /var/lib/apt/lists/*

複製程式碼

用到 build-essential 或者 gcc 系的也可以類似地處理:

RUN buildDeps="curl wget build-essentials flex bison make cmake autoconf automake git libtool" && \
    fetchDeps="nano wget curl"
    apt-get update && \
    apt-get install -y --no-install-recommends $fetchDeps && \
    AUTO_ADDED_PACKAGES=`apt-mark showauto` && \
    apt-get install -y --no-install-recommends $buildDeps && \
    mkdir build && cd build && cmake .. && make && make install && \
    apt-get purge -y --auto-remove $buildDeps $AUTO_ADDED_PACKAGES && \
    rm -rf /var/lib/apt/lists/*
複製程式碼

注意到我們採用了 AUTO_ADDED_PACKAGES 機制,這是一種 Debian 包管理系的記憶功能,可以被用來很好地削減尺寸。

centos 的 yum 系慣用法

類似 apt,不再贅述了

多遍構建

儘管包管理的記憶功能能夠完美地削減容器尺寸,但它並非是沒有缺點的:

  1. 你必須在單句 RUN 中寫出記憶以及消除記憶的全部指令碼,如果分割到多句指令,那麼容器中的 OS的佔地面積依然能被收縮,但容器的尺寸可能並不能被削減。

  2. 如果你在單句 RUN 指令中完成了你的整個容器構建指令碼的話,構建的開發過程將會非常痛苦,因為冗長的指令序列不能被快取到多層中,所以每一次微小的變化都會導致 docker build 去完整地重建你的這個容器。

    所以縮減容器尺寸,應該是當你的容器構建過程已經開發完成之後才去做的事情。

好的,記憶功能有點點不完美,但是多遍構建能夠很好地平衡這一切問題。

以 golang 應用的容器化為例,下面是一個多遍構建的例子:

FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go    .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
# 摘自 https://github.com/alexellis/href-counter
複製程式碼

好吧,我自己的寫的複雜得多,但暫時還不能展示,此外,複雜的版本也不利於闡述骨架結構。

結束

寫到這裡,暫時告一段落了。

關於縮減尺寸以及 Dockerfile 的慣用寫法,也就先說這麼多了。再要釋出點什麼也不是不可以,但可能涉及到的就不是僅僅 Docker 的知識了。

結果還是分了分章節,哎呀德性了

相關文章