Dockerfile Idioms
分發更小的容器,是一種應該被推薦的行為。
更小的容器,啟動會更快,分發會更快,重用會更快,……。對於生命來說,快是一種態度,代表你的時間被消費的更有價值:節約時間總是正確的。
幾年前,其實我就想寫有關容器縮減的內容,不過那時候 alpine 還不被重視,它自己也很難用,所以我們更多地是在 ubuntu 這樣的大塊頭上研究怎麼削減最終尺寸。
值得欣慰的是,那些曾經用到的原則一直都是對的,儘管這個世界、這些生態在變,但準則沒有變。
現在,關於容器尺寸削減的問題,文章多得很,問答也很多。
但我還是打算寫一篇。寫者嘛,總是覺得自己寫的內容對一點,邏輯對一點,用詞對一點,覆蓋面對一點,限定語對一點。等等。
不過,這次準備隨便寫,不打算整構邏輯了。
Checklist
削減容器的最終尺寸,首先考慮如下的 Checklist:
-
採用更小的基準包
- 在絕大多數情況下,alpine是最佳選擇
- 極端情況下可以使用 distroless,但後果是沒有shell,無法進入容器
- 有的時候你可以使用 busybox
-
選取最恰當的基準包。
- 對於 golang 服務來說,
alpine:latest
是最佳選擇- 但如果你需要golang構建操作,則
golang:alpine
可能才是對的
- 但如果你需要golang構建操作,則
- 對於 java 系列來說,這些都是好選擇:
- anapsix/alpine-java
- frolvlad/alpine-java
- openjdk
- 這些也可以參考:
- 無論哪一個,都是巨的一逼。
- 加上 SpringCloud 之後,更是2B。
- 選 Java 已經是錯了。
- 對於 nodejs 來說,alpine 版本是最佳的:
node:8-alpine
- 等等,沒法一一按語言列舉。
- 對於 golang 服務來說,
-
使用包安裝命令時,記住清除包安裝過程所下載的索引、安裝包
後面我會在慣用法中更多介紹這一點。
-
去掉記憶體交換機制,去掉交換分割槽
-
不要安裝帶有 ncurse 依賴的工具,例如 mc
-
不要安裝帶有除錯工具或者除錯工具性質的工具,例如 vim,curl。一定要用,使用 nano 和 wget 替代它們
-
調整命令順序,合併相同命令,使得產生更少的層
-
使用記憶功能以便去掉打包過程中才會使用的包,從而縮減最終容器尺寸
這個記憶功能,主要是指
alpine apk --virtual
功能;對於 apt 則有一個 apt-mark 工具。 -
對於 apt 使用
apt install --no-install-recommends -y
方式 -
使用多遍構建過程,將打包和中間內容排除在最終容器之外,以縮減其尺寸
下面都是基於 voxr vdeps-base 來介紹。
各種慣用法
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 clean
和 rm /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,不再贅述了
多遍構建
儘管包管理的記憶功能能夠完美地削減容器尺寸,但它並非是沒有缺點的:
-
你必須在單句
RUN
中寫出記憶以及消除記憶的全部指令碼,如果分割到多句指令,那麼容器中的 OS的佔地面積依然能被收縮,但容器的尺寸可能並不能被削減。 -
如果你在單句
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 的知識了。
結果還是分了分章節,哎呀德性了