減小 Docker 鏡像體積

在構(gòu)建 Docker 容器時(shí),應(yīng)該盡量想辦法獲得體積更小的鏡像,因?yàn)閭鬏敽筒渴痼w積較小的鏡像速度更快途茫。

但 RUN 語句總是會(huì)創(chuàng)建一個(gè)新層,而且在生成鏡像之前還需要使用很多中間文件溪食,在這種情況下囊卜,該如何獲得體積更小的鏡像呢?

你可能已經(jīng)注意到了错沃,大多數(shù) Dockerfiles 都使用了一些奇怪的技巧:

FROM ubuntu
RUN apt-get update && apt-get install vim

為什么使用 && 栅组?而不是使用兩個(gè) RUN 語句代替呢?比如:

FROM ubuntu
RUN apt-get update
RUN apt-get install vim

從 Docker 1.10 開始枢析,COPY玉掸、ADD 和 RUN 語句會(huì)向鏡像中添加新層。前面的示例創(chuàng)建了兩個(gè)層而不是一個(gè)醒叁。

鏡像的層添加

鏡像的層就像 Git 的提交(commit)一樣司浪。

Docker 的層 用于保存鏡像的上一版本和當(dāng)前版本之間的差異。就像 Git 的提交一樣把沼,如果你與其他存儲(chǔ)庫或鏡像共享它們啊易,就會(huì)很方便。

實(shí)際上智政,當(dāng)你向注冊表請求鏡像時(shí)认罩,只是下載你尚未擁有的層。這是一種非常高效地共享鏡像的方式续捂。但額外的層并不是沒有代價(jià)的垦垂。層仍然會(huì)占用空間,你擁有的層越多牙瓢,最終的鏡像就越大劫拗。Git 存儲(chǔ)庫在這方面也是類似的,存儲(chǔ)庫的大小隨著層數(shù)的增加而增加矾克,因?yàn)?Git 必須保存提交之間的所有變更页慷。

過去,將多個(gè) RUN 語句組合在一行命令中或許是一種很好的做法胁附,就像上面的第一個(gè)例子那樣酒繁,但在現(xiàn)在看來,這樣做并不妥控妻。

1州袒、通過 Docker 多階段構(gòu)建將多個(gè)層壓縮為一個(gè)


當(dāng) Git 存儲(chǔ)庫變大時(shí),你可以選擇將歷史提交記錄壓縮為單個(gè)提交弓候。事實(shí)證明郎哭,在 Docker 中也可以使用多階段構(gòu)建達(dá)到類似的目的他匪。

在這個(gè)示例中,你將構(gòu)建一個(gè) Node.js 容器夸研。讓我們從 index.js 開始:

const express = require('express')
const app = express()
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(3000, () => {
  console.log(`Example app listening on port 3000!`)
})

package.json

{
  "name": "hello-world",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "express": "^4.16.2"
  },
  "scripts": {
    "start": "node index.js"
  }
}

你可以使用下面的 Dockerfile 來打包這個(gè)應(yīng)用程序:


FROM node:8
EXPOSE 3000
WORKDIR /app
COPY package.json index.js ./
RUN npm install
CMD ["npm", "start"]

然后開始構(gòu)建鏡像:

$ docker build -t node-vanilla .

參數(shù):

-t, --tag value :命名一個(gè) tag 為 name:tag 格式(默認(rèn)為 [])

然后用以下方法驗(yàn)證它是否可以正常運(yùn)行:

$ docker run -p 3000:3000 -ti --rm --init node-vanilla

參數(shù)說明:

  • -p, --publish value:將容器的端口映射到主機(jī)端口(默認(rèn)為 [])

  • -t, --tty:分配一個(gè)偽 TTY

  • -i, --interactive:保持標(biāo)準(zhǔn)輸入

  • --rm:當(dāng)容器退出時(shí)自動(dòng)刪除

你應(yīng)該能訪問 http://localhost:3000邦蜜,并收到 "Hello World!"。

Dockerfile 中使用了一個(gè) COPY 語句和一個(gè) RUN 語句亥至,所以按照預(yù)期悼沈,新鏡像應(yīng)該比基礎(chǔ)鏡像多出至少兩個(gè)層:

$ docker history node-vanilla

IMAGE          CREATED BY                                      SIZE
075d229d3f48   /bin/sh -c #(nop)  CMD ["npm" "start"]          0B
bc8c3cc813ae   /bin/sh -c npm install                          2.91MB
bac31afb6f42   /bin/sh -c #(nop) COPY multi:3071ddd474429e1…   364B
500a9fbef90e   /bin/sh -c #(nop) WORKDIR /app                  0B
78b28027dfbf   /bin/sh -c #(nop)  EXPOSE 3000                  0B
b87c2ad8344d   /bin/sh -c #(nop)  CMD ["node"]                 0B
<missing>      /bin/sh -c set -ex   && for key in     6A010…   4.17MB
<missing>      /bin/sh -c #(nop)  ENV YARN_VERSION=1.3.2       0B
<missing>      /bin/sh -c ARCH= && dpkgArch="$(dpkg --print…   56.9MB
<missing>      /bin/sh -c #(nop)  ENV NODE_VERSION=8.9.4       0B
<missing>      /bin/sh -c set -ex   && for key in     94AE3…   129kB
<missing>      /bin/sh -c groupadd --gid 1000 node   && use…   335kB
<missing>      /bin/sh -c set -ex;  apt-get update;  apt-ge…   324MB
<missing>      /bin/sh -c apt-get update && apt-get install…   123MB
<missing>      /bin/sh -c set -ex;  if ! command -v gpg > /…   0B
<missing>      /bin/sh -c apt-get update && apt-get install…   44.6MB
<missing>      /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>      /bin/sh -c #(nop) ADD file:1dd78a123212328bd…   123MB

但實(shí)際上,生成的鏡像多了五個(gè)新層:每一個(gè)層對應(yīng) Dockerfile 里的一個(gè)語句抬闯。

現(xiàn)在井辆,讓我們來試試 Docker 的多階段構(gòu)建

你可以繼續(xù)使用與上面相同的 Dockerfile溶握,只是現(xiàn)在要調(diào)用兩次:

FROM node:8 as build
WORKDIR /app
COPY package.json index.js ./
RUN npm install
FROM node:8
COPY --from=build /app /
EXPOSE 3000
CMD ["index.js"]

Dockerfile 的第一部分創(chuàng)建了三個(gè)層,然后這些層被合并并復(fù)制到第二個(gè)階段蒸播。在第二階段睡榆,鏡像頂部又添加了額外的兩個(gè)層,所以總共是三個(gè)層袍榆。

多階段構(gòu)建

現(xiàn)在來驗(yàn)證一下胀屿。首先,構(gòu)建容器:

$ docker build -t node-multi-stage .

查看鏡像的歷史:

$ docker history node-multi-stage

IMAGE          CREATED BY                                      SIZE
331b81a245b1   /bin/sh -c #(nop)  CMD ["index.js"]             0B
bdfc932314af   /bin/sh -c #(nop)  EXPOSE 3000                  0B
f8992f6c62a6   /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77…   1.62MB
b87c2ad8344d   /bin/sh -c #(nop)  CMD ["node"]                 0B
<missing>      /bin/sh -c set -ex   && for key in     6A010…   4.17MB
<missing>      /bin/sh -c #(nop)  ENV YARN_VERSION=1.3.2       0B
<missing>      /bin/sh -c ARCH= && dpkgArch="$(dpkg --print…   56.9MB
<missing>      /bin/sh -c #(nop)  ENV NODE_VERSION=8.9.4       0B
<missing>      /bin/sh -c set -ex   && for key in     94AE3…   129kB
<missing>      /bin/sh -c groupadd --gid 1000 node   && use…   335kB
<missing>      /bin/sh -c set -ex;  apt-get update;  apt-ge…   324MB
<missing>      /bin/sh -c apt-get update && apt-get install…   123MB
<missing>      /bin/sh -c set -ex;  if ! command -v gpg > /…   0B
<missing>      /bin/sh -c apt-get update && apt-get install…   44.6MB
<missing>      /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>      /bin/sh -c #(nop) ADD file:1dd78a123212328bd…   123MB

文件大小是否已發(fā)生改變包雀?

$ docker images | grep node-
node-multi-stage   331b81a245b1   678MB
node-vanilla       075d229d3f48   679MB

最后一個(gè)鏡像(node-multi-stage)更小一些宿崭。你已經(jīng)將鏡像的體積減小了,即使它已經(jīng)是一個(gè)很小的應(yīng)用程序才写。

但整個(gè)鏡像仍然很大葡兑!有什么辦法可以讓它變得更小嗎?

2赞草、用 distroless 去除容器中所有不必要的東西


這個(gè)鏡像包含了 Node.js 以及 yarn讹堤、npm、bash 和其他的二進(jìn)制文件厨疙。因?yàn)樗彩腔?Ubuntu 的洲守,所以你等于擁有了一個(gè)完整的操作系統(tǒng),其中包括所有的小型二進(jìn)制文件和實(shí)用程序沾凄。

但在運(yùn)行容器時(shí)是不需要這些東西的梗醇,你需要的只是 Node.js。Docker 容器應(yīng)該只包含一個(gè)進(jìn)程以及用于運(yùn)行這個(gè)進(jìn)程所需的最少的文件撒蟀,你不需要整個(gè)操作系統(tǒng)叙谨。

實(shí)際上,你可以刪除 Node.js 之外的所有內(nèi)容牙肝。但要怎么做唉俗?所幸的是嗤朴,谷歌為我們提供了distroless

以下是 distroless 存儲(chǔ)庫的描述:

distroless 鏡像只包含應(yīng)用程序及其運(yùn)行時(shí)依賴項(xiàng)虫溜,不包含程序包管理器雹姊、shell 以及在標(biāo)準(zhǔn) Linux 發(fā)行版中可以找到的任何其他程序。

這正是你所需要的衡楞!你可以對 Dockerfile 進(jìn)行調(diào)整吱雏,以利用新的基礎(chǔ)鏡像,如下所示:

FROM node:8 as build
WORKDIR /app
COPY package.json index.js ./
RUN npm install
FROM gcr.io/distroless/nodejs
COPY --from=build /app /
EXPOSE 3000
CMD ["index.js"]

你可以像往常一樣編譯鏡像:

$ docker build -t node-distroless .

這個(gè)鏡像應(yīng)該能正常運(yùn)行瘾境。要驗(yàn)證它歧杏,可以像這樣運(yùn)行容器:

$ docker run -p 3000:3000 -ti --rm --init node-distroless

現(xiàn)在可以訪問 [http://localhost:3000](http://localhost:3000/) 頁面。不包含其他額外二進(jìn)制文件的鏡像是不是小多了迷守?

$ docker images | grep node-distroless
node-distroless   7b4db3b7f1e5   76.7MB

只有 76.7MB犬绒!比之前的鏡像小了 600MB!但在使用 distroless 時(shí)有一些事項(xiàng)需要注意兑凿。

當(dāng)容器在運(yùn)行時(shí)凯力,如果你想要檢查它,可以使用以下命令 attach 到正在運(yùn)行的容器上:

$ docker exec -ti <insert_docker_id> bash

attach 到正在運(yùn)行的容器并運(yùn)行 bash 命令就像是建立了一個(gè) SSH 會(huì)話一樣礼华。

但 distroless 版本是原始操作系統(tǒng)的精簡版咐鹤,沒有了額外的二進(jìn)制文件,所以容器里沒有 shell圣絮!在沒有 shell 的情況下祈惶,如何 attach 到正在運(yùn)行的容器呢?答案是扮匠,你做不到捧请。這既是個(gè)壞消息,也是個(gè)好消息餐禁。

之所以說是壞消息血久,因?yàn)槟阒荒茉谌萜髦袌?zhí)行二進(jìn)制文件。你可以運(yùn)行的唯一的二進(jìn)制文件是 Node.js:

$ docker exec -ti <insert_docker_id> node

說它是個(gè)好消息帮非,是因?yàn)槿绻粽呃媚愕膽?yīng)用程序獲得對容器的訪問權(quán)限將無法像訪問 shell 那樣造成太多破壞氧吐。換句話說,更少的二進(jìn)制文件意味著更小的體積和更高的安全性末盔,不過這是以痛苦的調(diào)試為代價(jià)的筑舅。

或許你不應(yīng)在生產(chǎn)環(huán)境中 attach 和調(diào)試容器,而應(yīng)該使用日志和監(jiān)控陨舱。但如果你確實(shí)需要調(diào)試翠拣,又想保持小體積該怎么辦?

3游盲、小體積的 Alpine 基礎(chǔ)鏡像


你可以使用 Alpine 基礎(chǔ)鏡像替換 distroless 基礎(chǔ)鏡像误墓。

Alpine Linux 是:

一個(gè)基于 musl libc 和 busybox 的面向安全的輕量級 Linux 發(fā)行版蛮粮。

換句話說,它是一個(gè)體積更小也更安全的 Linux 發(fā)行版谜慌。不過你不應(yīng)該理所當(dāng)然地認(rèn)為他們聲稱的就一定是事實(shí)然想,讓我們來看看它的鏡像是否更小。

先修改 Dockerfile欣范,讓它使用 node:8-alpine:

FROM node:8 as build
WORKDIR /app
COPY package.json index.js ./
RUN npm install
FROM node:8-alpine
COPY --from=build /app /
EXPOSE 3000
CMD ["npm", "start"]

使用下面的命令構(gòu)建鏡像:

$ docker build -t node-alpine .

現(xiàn)在可以檢查一下鏡像大斜湫埂:

$ docker images | grep node-alpine
node-alpine   aa1f85f8e724   69.7MB

69.7MB!甚至比 distrless 鏡像還心涨怼妨蛹!現(xiàn)在可以 attach 到正在運(yùn)行的容器嗎?讓我們來試試晴竞。

讓我們先啟動(dòng)容器:

$ docker run -p 3000:3000 -ti --rm --init node-alpine
Example app listening on port 3000!

你可以使用以下命令 attach 到運(yùn)行中的容器:

$ docker exec -ti 9d8e97e307d7 bash
OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown

看來不行蛙卤,但或許可以使用 shell?

$ docker exec -ti 9d8e97e307d7 sh / #

成功了噩死!現(xiàn)在可以 attach 到正在運(yùn)行的容器中了表窘。

看起來很有希望,但還有一個(gè)問題甜滨。

Alpine 基礎(chǔ)鏡像是基于 muslc 的——C 語言的一個(gè)替代標(biāo)準(zhǔn)庫,而大多數(shù) Linux 發(fā)行版如 Ubuntu瘤袖、Debian 和 CentOS 都是基于 glibc 的衣摩。這兩個(gè)庫應(yīng)該實(shí)現(xiàn)相同的內(nèi)核接口。

但它們的目的是不一樣的:

  • glibc 更常見捂敌,速度也更快艾扮;

  • muslc 使用較少的空間,并側(cè)重于安全性占婉。

在編譯應(yīng)用程序時(shí)泡嘴,大部分都是針對特定的 libc 進(jìn)行編譯的。如果你要將它們與另一個(gè) libc 一起使用逆济,則必須重新編譯它們酌予。換句話說,基于 Alpine 基礎(chǔ)鏡像構(gòu)建容器可能會(huì)導(dǎo)致非預(yù)期的行為奖慌,因?yàn)闃?biāo)準(zhǔn) C 庫是不一樣的抛虫。

你可能會(huì)注意到差異,特別是當(dāng)你處理預(yù)編譯的二進(jìn)制文件(如 Node.js C++ 擴(kuò)展)時(shí)简僧。例如建椰,PhantomJS 的預(yù)構(gòu)建包就不能在 Alpine 上運(yùn)行。

你應(yīng)該選擇哪個(gè)基礎(chǔ)鏡像岛马?你應(yīng)該使用 Alpine棉姐、distroless 還是原始鏡像屠列?

如果你是在生產(chǎn)環(huán)境中運(yùn)行容器,并且更關(guān)心安全性伞矩,那么可能 distroless 鏡像更合適笛洛。添加到 Docker 鏡像的每個(gè)二進(jìn)制文件都會(huì)給整個(gè)應(yīng)用程序增加一定的風(fēng)險(xiǎn)。只在容器中安裝一個(gè)二進(jìn)制文件可以降低總體風(fēng)險(xiǎn)扭吁。

例如撞蜂,如果攻擊者能夠利用運(yùn)行在 distroless 上的應(yīng)用程序的漏洞,他們將無法在容器中使用 shell侥袜,因?yàn)槟抢锔揪蜎]有 shell蝌诡!

請注意,OWASP 本身就建議盡量減少攻擊表面枫吧。

如果你只關(guān)心更小的鏡像體積浦旱,那么可以考慮基于 Alpine 的鏡像。它們的體積非常小九杂,但代價(jià)是兼容性較差颁湖。Alpine 使用了略微不同的標(biāo)準(zhǔn) C 庫——muslc。你可能會(huì)時(shí)不時(shí)地遇到一些兼容性問題例隆。

原始基礎(chǔ)鏡像非常適合用于測試和開發(fā)甥捺。它雖然體積很大,但提供了與 Ubuntu 工作站一樣的體驗(yàn)镀层。此外镰禾,你還可以訪問操作系統(tǒng)的所有二進(jìn)制文件。

再回顧一下各個(gè)鏡像的大谐辍:

node:8  681MB
node:8  使用多階段構(gòu)建為 678MB
gcr.io/distroless/nodejs  76.7MB
node:8-alpine  69.7MB

轉(zhuǎn)自于:https://www.infoq.cn/article/3-simple-tricks-for-smaller-docker-images
英文原文:https://itnext.io/3-simple-tricks-for-smaller-docker-images-f0d2bda17d1e

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末吴侦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子坞古,更是在濱河造成了極大的恐慌备韧,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痪枫,死亡現(xiàn)場離奇詭異织堂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)听怕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進(jìn)店門捧挺,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人尿瞭,你說我怎么就攤上這事闽烙。” “怎么了?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵黑竞,是天一觀的道長捕发。 經(jīng)常有香客問我,道長很魂,這世上最難降的妖魔是什么扎酷? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮遏匆,結(jié)果婚禮上法挨,老公的妹妹穿的比我還像新娘。我一直安慰自己幅聘,他們只是感情好凡纳,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著帝蒿,像睡著了一般荐糜。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上葛超,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天暴氏,我揣著相機(jī)與錄音,去河邊找鬼绣张。 笑死答渔,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的侥涵。 我是一名探鬼主播研儒,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼独令!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起好芭,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤燃箭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后舍败,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體招狸,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年邻薯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了裙戏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,563評論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡厕诡,死狀恐怖累榜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤壹罚,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布葛作,位于F島的核電站,受9級特大地震影響猖凛,放射性物質(zhì)發(fā)生泄漏赂蠢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一辨泳、第九天 我趴在偏房一處隱蔽的房頂上張望虱岂。 院中可真熱鬧,春花似錦菠红、人聲如沸第岖。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绍傲。三九已至,卻和暖如春耍共,著一層夾襖步出監(jiān)牢的瞬間烫饼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工试读, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杠纵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓钩骇,卻偏偏與公主長得像比藻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子倘屹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,435評論 2 348