在構建Docker容器時,您應該始終爭取較小的映像。共享圖層且尺寸較小的影象可以更快地傳輸和部署。
但是,當每個RUN語句建立一個新圖層時又如何控制大小,並且在準備好影象之前需要中間的偽像?
您可能已經注意到,大多數Dockerfile野外的s都有一些奇怪的技巧,例如:
Docker檔案
FROM ubuntuRUN apt-get update && apt-get install vim
為什麼&&呢?為什麼不執行兩個這樣的RUN語句?
Docker檔案
FROM ubuntuRUN apt-get updateRUN apt-get install vim
從Docker 1.10開始COPY,ADD和RUN語句向您的映像新增一個新層。前面的示例建立了兩層而不是一層。
層就像git commits。
Docker層儲存映像的先前版本和當前版本之間的差異。和git commits一樣,如果您與其他儲存庫或影象共享它們,它們將很方便。
實際上,當您從登錄檔中請求影象時,您只會下載尚未擁有的圖層。這種方式更有效地共享影象。
但是圖層不是免費的。
圖層使用空間,並且圖層越多,最終影象就越重。Git儲存庫在這方面是相似的。儲存庫的大小隨層數的增加而增加,因為Git必須儲存提交之間的所有更改。
過去,將多個RUN語句合併在一行上是一個好習慣。像第一個例子一樣。
不再。
1.使用多階段Docker構建將多層壓縮為一層
當Git儲存庫變大時,您可以選擇將歷史記錄壓縮為一次提交,而無需考慮過去。
事實證明,您也可以通過多階段構建在Docker中執行類似的操作。
在此示例中,您將構建一個Node.js容器。
讓我們從開始 index.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:
package.json
{ "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" }}
您可以使用以下程式打包此應用程式Dockerfile:
Docker檔案
FROM node:8EXPOSE 3000WORKDIR /appCOPY package.json index.js ./RUN npm installCMD ["npm", "start"]
您可以使用以下方法構建影象:
重擊
docker build -t node-vanilla .
您可以測試它是否可以正確使用:
重擊
docker run -p 3000:3000 -ti --rm --init node-vanilla> [email protected] start /app> node index.jsExample app listening on port 3000!
您應該能夠訪問http:// localhost:3000並受到“ Hello World!”的歡迎。。
中有COPY和RUN語句Dockerfile。因此,您應該期望看到的影象至少比基本影象多兩層:
重擊
docker history node-vanillaIMAGE CREATED BY SIZE075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 0Bbc8c3cc813ae /bin/sh -c npm install 2.91MBbac31afb6f42 /bin/sh -c #(nop) COPY multi:3071ddd474429e1… 364B500a9fbef90e /bin/sh -c #(nop) WORKDIR /app 0B78b28027dfbf /bin/sh -c #(nop) EXPOSE 3000 0Bb87c2ad8344d /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
相反,生成的影象具有五個新層:層中的每個語句一層Dockerfile。
讓我們嘗試多階段Docker構建。
您將使用Dockerfile上面的相同內容,但是兩次:
Docker檔案
FROM node:8 as buildWORKDIR /appCOPY package.json index.js ./RUN npm installFROM node:8COPY --from=build /app /EXPOSE 3000CMD ["index.js"]
的第一部分Dockerfile建立三層。然後將各層合併並複製到第二個也是最後一個階段。在影象頂部再新增兩層,總共3層。
繼續驗證自己。首先,構建容器:
重擊
docker build -t node-multi-stage .
現在檢查歷史記錄:
重擊
docker history node-multi-stageIMAGE CREATED BY SIZE331b81a245b1 /bin/sh -c #(nop) CMD ["index.js"] 0Bbdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0Bf8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MBb87c2ad8344d /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
歡呼!檔案大小有沒有改變?
重擊
docker images | grep node-node-multi-stage 331b81a245b1 678MBnode-vanilla 075d229d3f48 679MB
是的,最後一張影象稍小。
還不錯!即使這是已經精簡的應用程式,您也減小了整體尺寸。
但是形象還是很大的!
您有什麼可以做得更小?
2.從容器中取出所有不必要的碎屑
當前影象船舶的Node.js以及yarn,npm,bash和許多其他的二進位制檔案。它也基於Ubuntu。因此,您將擁有一個完整的作業系統,其中包含所有小的二進位制檔案和實用程式。
執行容器時,您不需要任何這些。您唯一需要的依賴項是Node.js。
Docker容器應包裝一個程序幷包含執行它的最低限度的內容。您不需要作業系統。
但是如何?
幸運的是,Google擁有相同的想法,並提出了GoogleCloudPlatform / distroless。
正如儲存庫的說明所指出的:
“無發行版”映像僅包含您的應用程式及其執行時依賴項。它們不包含程式包管理器,不會包含您希望在標準Linux發行版中找到的任何其他程式。
這正是您所需要的!
您可以進行如下調整Dockerfile以利用新的基本映像:
Docker檔案
FROM node:8 as buildWORKDIR /appCOPY package.json index.js ./RUN npm installFROM gcr.io/distroless/nodejsCOPY --from=build /app /EXPOSE 3000CMD ["index.js"]
您可以照常使用以下方法編譯映像:
重擊
docker build -t node-distroless .
該應用程式應正常執行。要驗證是否仍然存在,可以像這樣執行容器:
重擊
docker run -p 3000:3000 -ti --rm --init node-distroless
並訪問http:// localhost:3000上的頁面。
沒有所有額外二進位制檔案的影象會變小嗎?
重擊
docker images | grep node-distrolessnode-distroless 7b4db3b7f1e5 76.7MB
只有76.7MB!
比以前的影象少600MB!
好訊息!但是當涉及到災難時,您需要注意一些事項。
當您的容器正在執行時,並且希望對其進行檢查時,可以使用以下方法將容器連線到正在執行的容器:
重擊
docker exec -ti <insert_docker_id> bash
附加到正在執行的容器並開始執行bash就像建立SSH會話一樣。
但是由於distroless是原始作業系統的簡化版本,因此沒有多餘的二進位制檔案。容器中沒有外殼!
如果沒有外殼,如何連線到正在執行的容器?
好訊息和壞訊息是你做不到。
這是個壞訊息,因為您只能在容器中執行二進位制檔案。您可以執行的唯一二進位制檔案是Node.js:
重擊
docker exec -ti <insert_docker_id> node
這是個好訊息,因為利用您的應用程式並獲得對容器的訪問權的攻擊者所造成的破壞不會像訪問Shell那樣造成太大的損失。換句話說,更少的二進位制檔案意味著更小的大小和更高的安全性。但是以更痛苦的除錯為代價。
請注意,也許您不應該在生產環境中附加和除錯容器。您應該依靠適當的日誌記錄和監視。
但是,如果您關心除錯和較小尺寸該怎麼辦?
3.使用Alpine的基礎影象更小
您可以使用基於Alpine的影象替換非發行版的基本影象。
高山Linux是:
基於musl libc和busybox的面向安全的輕量級Linux發行版
換句話說,Linux發行版的大小更小且更安全。
你不應該認為他們的話是理所當然的。讓我們檢查影象是否較小。
您應該調整Dockerfile和使用node:8-alpine:
Docker檔案
FROM node:8 as buildWORKDIR /appCOPY package.json index.js ./RUN npm installFROM node:8-alpineCOPY --from=build /app /EXPOSE 3000CMD ["npm", "start"]
您可以使用以下方法構建影象:
重擊
docker build -t node-alpine .
您可以使用以下方法檢查尺寸:
重擊
docker images | grep node-alpinenode-alpine aa1f85f8e724 69.7MB
69.7MB!
甚至比一成不變的形象還要小!
與distroless不同,您可以連線到正在執行的容器嗎?是時候找出答案了。
讓我們先啟動容器:
重擊
docker run -p 3000:3000 -ti --rm --init node-alpineExample app listening on port 3000!
您可以使用以下方法將容器連線到正在執行的容器:
重擊
docker exec -ti 9d8e97e307d7 bashOCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \\"bash\\": executable file not found in $PATH": unknown
沒有運氣。但是,也許容器有問題sh?
重擊
docker exec -ti 9d8e97e307d7 sh/ #/ # exit
是! 您仍然可以附加到正在執行的容器,並且總體上較小。
聽起來很有希望,但有一個陷阱。
基於高山的影象基於muslc(C語言的替代標準庫)。
但是,大多數Linux發行版(例如Ubuntu,Debian和CentOS)都基於glibc。這兩個庫應該實現與核心相同的介面。
但是,它們有不同的目標:
glibc是最常見和最快的muslc使用更少的空間並且在編寫時考慮到安全性編譯應用程式時,大部分情況是針對特定的libc進行編譯的。如果您希望將它們與另一個libc一起使用,則必須重新編譯它們。
換句話說,由於標準C庫不同,使用Alpine影象構建容器可能會導致意外行為。
您可能會注意到差異,尤其是在處理預編譯的二進位制檔案(例如Node.js C ++擴充套件)時。
例如,PhantomJS預先構建的軟體包在Alpine上不起作用。
您應該選擇哪種基本影象?
您是否使用Alpine,Distroless或Vanilla圖片?
如果您正在生產環境中執行並且擔心安全性,那麼可能要使用堅硬的映像。
新增到Docker映像的每個二進位制檔案都會給整個應用程式帶來一定程度的風險。
通過在容器中僅安裝一個二進位制檔案,可以降低總體風險。
例如,如果攻擊者能夠利用在Distroless上執行的應用程式中的漏洞,則他們將無法在容器中生成外殼,因為沒有外殼!
請注意,OWASP建議最小化攻擊表面積。
如果您不惜一切代價擔心尺寸,則應切換到基於Alpine的圖片。
這些通常很小,但是以相容性為代價。Alpine使用略有不同的標準C庫-muslc。您可能會不時遇到一些相容性問題。此處的更多示例“ alpine-node docker image和google-cloud等於載入ld-linux-x86-64.so.2時出錯”,此處“匯入試圖在alpine上執行gRPC的錯誤”。
香草基礎影象非常適合測試和開發。
它很大,但提供的體驗與要安裝Ubuntu的工作站相同。另外,您還可以訪問作業系統中所有可用的二進位制檔案。
影象尺寸回顧:
圖片大小(MB)node:8681node:8 多階段構建678gcr.io/distroless/nodejs76.7node:8-alpine69.7