編寫最佳的Dockerfile的方法
Dockerfile的語(yǔ)法非常簡(jiǎn)單,然而如何加快鏡像構(gòu)建速度,如何減少Docker鏡像的大小卻不是那么直觀,需要積累實(shí)踐經(jīng)驗(yàn)。這篇博客可以幫助你快速掌握編寫Dockerfile的技巧。
為了保證可讀性,本文采用意譯而非直譯。另外,本文版權(quán)歸原作者所有,翻譯僅用于學(xué)習(xí)。
我已經(jīng)使用Docker有一段時(shí)間了,其中編寫Dockerfile是非常重要的一部分工作。在這篇博客中,我打算分享一些建議,幫助大家編寫更好的Dockerfile。
目標(biāo):
- 更快的構(gòu)建速度
- 更小的Docker鏡像大小
- 更少的Docker鏡像層
- 充分利用鏡像緩存
- 增加Dockerfile可讀性
- 讓Docker容器使用起來(lái)更簡(jiǎn)單
總結(jié)
- 編寫.dockerignore文件
- 容器只運(yùn)行單個(gè)應(yīng)用
- 將多個(gè)RUN指令合并為一個(gè)
- 基礎(chǔ)鏡像的標(biāo)簽不要用latest
- 每個(gè)RUN指令后刪除多余文件
- 選擇合適的基礎(chǔ)鏡像(alpine版本最好)
- 設(shè)置WORKDIR和CMD
- 使用ENTRYPOINT (可選)
- 在entrypoint腳本中使用exec
- COPY與ADD優(yōu)先使用前者
- 合理調(diào)整COPY與RUN的順序
- 設(shè)置默認(rèn)的環(huán)境變量,映射端口和數(shù)據(jù)卷
- 使用LABEL設(shè)置鏡像元數(shù)據(jù)
- 添加HEALTHCHECK
示例
示例Dockerfile犯了幾乎所有的錯(cuò)(當(dāng)然我是故意的)。接下來(lái),我會(huì)一步步優(yōu)化它。假設(shè)我們需要使用Docker運(yùn)行一個(gè)Node.js應(yīng)用,下面就是它的Dockerfile(CMD指令太復(fù)雜了,所以我簡(jiǎn)化了,它是錯(cuò)誤的,僅供參考)。
FROM ubuntu ADD . /app RUN apt-get update RUN apt-get upgrade -y RUN apt-get install -y nodejs ssh mysql RUN cd /app && npm install # this should start three processes, mysql and ssh # in the background and node app in foreground # isn't it beautifully terrible? <3 CMD mysql & sshd & npm start
構(gòu)建鏡像:
docker build -t wtf .
1. 編寫.dockerignore文件
構(gòu)建鏡像時(shí),Docker需要先準(zhǔn)備context ,將所有需要的文件收集到進(jìn)程中。默認(rèn)的context包含Dockerfile目錄中的所有文件,但是實(shí)際上,我們并不需要.git目錄,node_modules目錄等內(nèi)容。 .dockerignore 的作用和語(yǔ)法類似于 .gitignore,可以忽略一些不需要的文件,這樣可以有效加快鏡像構(gòu)建時(shí)間,同時(shí)減少Docker鏡像的大小。示例如下:
.git/ node_modules/
2. 容器只運(yùn)行單個(gè)應(yīng)用
從技術(shù)角度講,你可以在Docker容器中運(yùn)行多個(gè)進(jìn)程。你可以將數(shù)據(jù)庫(kù),前端,后端,ssh,supervisor都運(yùn)行在同一個(gè)Docker容器中。但是,這會(huì)讓你非常痛苦:
非常長(zhǎng)的構(gòu)建時(shí)間(修改前端之后,整個(gè)后端也需要重新構(gòu)建)
非常大的鏡像大小
多個(gè)應(yīng)用的日志難以處理(不能直接使用stdout,否則多個(gè)應(yīng)用的日志會(huì)混合到一起)
橫向擴(kuò)展時(shí)非常浪費(fèi)資源(不同的應(yīng)用需要運(yùn)行的容器數(shù)并不相同)
僵尸進(jìn)程問(wèn)題 - 你需要選擇合適的init進(jìn)程
因此,我建議大家為每個(gè)應(yīng)用構(gòu)建單獨(dú)的Docker鏡像,然后使用 Docker Compose 運(yùn)行多個(gè)Docker容器。
現(xiàn)在,我從Dockerfile中刪除一些不需要的安裝包,另外,SSH可以用docker exec替代。示例如下:
FROM ubuntu ADD . /app RUN apt-get update RUN apt-get upgrade -y # we should remove ssh and mysql, and use # separate container for database RUN apt-get install -y nodejs # ssh mysql RUN cd /app && npm install CMD npm start
3. 將多個(gè)RUN指令合并為一個(gè)
Docker鏡像是分層的,下面這些知識(shí)點(diǎn)非常重要:
Dockerfile中的每個(gè)指令都會(huì)創(chuàng)建一個(gè)新的鏡像層。
鏡像層將被緩存和復(fù)用
當(dāng)Dockerfile的指令修改了,復(fù)制的文件變化了,或者構(gòu)建鏡像時(shí)指定的變量不同了,對(duì)應(yīng)的鏡像層緩存就會(huì)失效
某一層的鏡像緩存失效之后,它之后的鏡像層緩存都會(huì)失效
鏡像層是不可變的,如果我們?cè)倌骋粚又刑砑右粋€(gè)文件,然后在下一層中刪除它,則鏡像中依然會(huì)包含該文件(只是這個(gè)文件在Docker容器中不可見了)。
Docker鏡像類似于洋蔥。它們都有很多層。為了修改內(nèi)層,則需要將外面的層都刪掉。記住這一點(diǎn)的話,其他內(nèi)容就很好理解了。
現(xiàn)在,我們將所有的RUN指令合并為一個(gè)。同時(shí)把a(bǔ)pt-get upgrade刪除,因?yàn)樗鼤?huì)使得鏡像構(gòu)建非常不確定(我們只需要依賴基礎(chǔ)鏡像的更新就好了)
FROM ubuntu ADD . /app RUN apt-get update \ && apt-get install -y nodejs \ && cd /app \ && npm install CMD npm start
記住一點(diǎn),我們只能將變化頻率一樣的指令合并在一起。將node.js安裝與npm模塊安裝放在一起的話,則每次修改源代碼,都需要重新安裝node.js,這顯然不合適。因此,正確的寫法是這樣的:
FROM ubuntu RUN apt-get update && apt-get install -y nodejs ADD . /app RUN cd /app && npm install CMD npm start
4. 基礎(chǔ)鏡像的標(biāo)簽不要用latest
當(dāng)鏡像沒(méi)有指定標(biāo)簽時(shí),將默認(rèn)使用latest 標(biāo)簽。因此, FROM ubuntu 指令等同于FROM ubuntu:latest。當(dāng)時(shí),當(dāng)鏡像更新時(shí),latest標(biāo)簽會(huì)指向不同的鏡像,這時(shí)構(gòu)建鏡像有可能失敗。如果你的確需要使用最新版的基礎(chǔ)鏡像,可以使用latest標(biāo)簽,否則的話,最好指定確定的鏡像標(biāo)簽。
示例Dockerfile應(yīng)該使用16.04作為標(biāo)簽。
FROM ubuntu:16.04 # it's that easy! RUN apt-get update && apt-get install -y nodejs ADD . /app RUN cd /app && npm install CMD npm start
5. 每個(gè)RUN指令后刪除多余文件
假設(shè)我們更新了apt-get源,下載,解壓并安裝了一些軟件包,它們都保存在/var/lib/apt/lists/目錄中。但是,運(yùn)行應(yīng)用時(shí)Docker鏡像中并不需要這些文件。我們最好將它們刪除,因?yàn)樗鼤?huì)使Docker鏡像變大。
示例Dockerfile中,我們可以刪除/var/lib/apt/lists/目錄中的文件(它們是由apt-get update生成的)。
FROM ubuntu:16.04 RUN apt-get update \ && apt-get install -y nodejs \ # added lines && rm -rf /var/lib/apt/lists/* ADD . /app RUN cd /app && npm install CMD npm start
6. 選擇合適的基礎(chǔ)鏡像(alpine版本最好)
在示例中,我們選擇了ubuntu作為基礎(chǔ)鏡像。但是我們只需要運(yùn)行node程序,有必要使用一個(gè)通用的基礎(chǔ)鏡像嗎?node鏡像應(yīng)該是更好的選擇。
FROM node ADD . /app # we don't need to install node # anymore and use apt-get RUN cd /app && npm install CMD npm start
更好的選擇是alpine版本的node鏡像。alpine是一個(gè)極小化的Linux發(fā)行版,只有4MB,這讓它非常適合作為基礎(chǔ)鏡像。
FROM node:7-alpine ADD . /app RUN cd /app && npm install CMD npm start
apk是Alpine的包管理工具。它與apt-get有些不同,但是非常容易上手。另外,它還有一些非常有用的特性,比如no-cache和 --virtual選項(xiàng),它們都可以幫助我們減少鏡像的大小。
7. 設(shè)置WORKDIR和 CMD
WORKDIR指令可以設(shè)置默認(rèn)目錄,也就是運(yùn)行RUN / CMD / ENTRYPOINT指令的地方。
CMD指令可以設(shè)置容器創(chuàng)建是執(zhí)行的默認(rèn)命令。另外,你應(yīng)該講命令寫在一個(gè)數(shù)組中,數(shù)組中每個(gè)元素為命令的每個(gè)單詞(參考官方文檔)。
FROM node:7-alpine WORKDIR /app ADD . /app RUN npm install CMD ["npm", "start"]
8. 使用ENTRYPOINT (可選)
ENTRYPOINT指令并不是必須的,因?yàn)樗鼤?huì)增加復(fù)雜度。ENTRYPOINT是一個(gè)腳本,它會(huì)默認(rèn)執(zhí)行,并且將指定的命令錯(cuò)誤其參數(shù)。它通常用于構(gòu)建可執(zhí)行的Docker鏡像。entrypoint.sh如下:
#!/usr/bin/env sh # $0 is a script name, # $1, $2, $3 etc are passed arguments # $1 is our command CMD=$1 case "$CMD" in "dev" ) npm install export NODE_ENV=development exec npm run dev ;; "start" ) # we can modify files here, using ENV variables passed in # "docker create" command. It can't be done during build process. echo "db: $DATABASE_ADDRESS" >> /app/config.yml export NODE_ENV=production exec npm start ;; * ) # Run custom command. Thanks to this line we can still use # "docker run our_image /bin/bash" and it will work exec $CMD ${@:2} ;; esac
示例Dockerfile:
FROM node:7-alpine WORKDIR /app ADD . /app RUN npm install ENTRYPOINT ["./entrypoint.sh"] CMD ["start"]
可以使用如下命令運(yùn)行該鏡像:
# 運(yùn)行開發(fā)版本 docker run our-app dev # 運(yùn)行生產(chǎn)版本 docker run our-app start # 運(yùn)行bash docker run -it our-app /bin/bash
9. 在entrypoint腳本中使用exec
在前文的entrypoint腳本中,我使用了exec命令運(yùn)行node應(yīng)用。不使用exec的話,我們則不能順利地關(guān)閉容器,因?yàn)镾IGTERM信號(hào)會(huì)被bash腳本進(jìn)程吞沒(méi)。exec命令啟動(dòng)的進(jìn)程可以取代腳本進(jìn)程,因此所有的信號(hào)都會(huì)正常工作。
10. COPY與ADD優(yōu)先使用前者
COPY指令非常簡(jiǎn)單,僅用于將文件拷貝到鏡像中。ADD相對(duì)來(lái)講復(fù)雜一些,可以用于下載遠(yuǎn)程文件以及解壓壓縮包(參考官方文檔)。
FROM node:7-alpine WORKDIR /app COPY . /app RUN npm install ENTRYPOINT ["./entrypoint.sh"] CMD ["start"]
11. 合理調(diào)整COPY與RUN的順序
我們應(yīng)該把變化最少的部分放在Dockerfile的前面,這樣可以充分利用鏡像緩存。
示例中,源代碼會(huì)經(jīng)常變化,則每次構(gòu)建鏡像時(shí)都需要重新安裝NPM模塊,這顯然不是我們希望看到的。因此我們可以先拷貝package.json,然后安裝NPM模塊,最后才拷貝其余的源代碼。這樣的話,即使源代碼變化,也不需要重新安裝NPM模塊。
FROM node:7-alpine WORKDIR /app COPY package.json /app RUN npm install COPY . /app ENTRYPOINT ["./entrypoint.sh"] CMD ["start"]
12. 設(shè)置默認(rèn)的環(huán)境變量,映射端口和數(shù)據(jù)卷
運(yùn)行Docker容器時(shí)很可能需要一些環(huán)境變量。在Dockerfile設(shè)置默認(rèn)的環(huán)境變量是一種很好的方式。另外,我們應(yīng)該在Dockerfile中設(shè)置映射端口和數(shù)據(jù)卷。示例如下:
FROM node:7-alpine ENV PROJECT_DIR=/app WORKDIR $PROJECT_DIR COPY package.json $PROJECT_DIR RUN npm install COPY . $PROJECT_DIR ENV MEDIA_DIR=/media \ NODE_ENV=production \ APP_PORT=3000 VOLUME $MEDIA_DIR EXPOSE $APP_PORT ENTRYPOINT ["./entrypoint.sh"] CMD ["start"]
ENV指令指定的環(huán)境變量在容器中可以使用。如果你只是需要指定構(gòu)建鏡像時(shí)的變量,你可以使用ARG指令。
13. 使用LABEL設(shè)置鏡像元數(shù)據(jù)
使用LABEL指令,可以為鏡像設(shè)置元數(shù)據(jù),例如鏡像創(chuàng)建者或者鏡像說(shuō)明。舊版的Dockerfile語(yǔ)法使用MAINTAINER指令指定鏡像創(chuàng)建者,但是它已經(jīng)被棄用了。有時(shí),一些外部程序需要用到鏡像的元數(shù)據(jù),例如nvidia-docker需要用到com.nvidia.volumes.needed。示例如下:
FROM node:7-alpine LABEL maintainer "jakub.skalecki@example.com" ...
14. 添加HEALTHCHECK
運(yùn)行容器時(shí),可以指定--restart always選項(xiàng)。這樣的話,容器崩潰時(shí),Docker守護(hù)進(jìn)程(docker daemon)會(huì)重啟容器。對(duì)于需要長(zhǎng)時(shí)間運(yùn)行的容器,這個(gè)選項(xiàng)非常有用。但是,如果容器的確在運(yùn)行,但是不可(陷入死循環(huán),配置錯(cuò)誤)用怎么辦?使用HEALTHCHECK指令可以讓Docker周期性的檢查容器的健康狀況。我們只需要指定一個(gè)命令,如果一切正常的話返回0,否則返回1。對(duì)HEALTHCHECK感興趣的話,可以參考這篇博客。示例如下:
FROM node:7-alpine LABEL maintainer "jakub.skalecki@example.com" ENV PROJECT_DIR=/app WORKDIR $PROJECT_DIR COPY package.json $PROJECT_DIR RUN npm install COPY . $PROJECT_DIR ENV MEDIA_DIR=/media \ NODE_ENV=production \ APP_PORT=3000 VOLUME $MEDIA_DIR EXPOSE $APP_PORT HEALTHCHECK CMD curl --fail http://localhost:$APP_PORT || exit 1 ENTRYPOINT ["./entrypoint.sh"] CMD ["start"]
當(dāng)請(qǐng)求失敗時(shí),curl --fail 命令返回非0狀態(tài)。
原文: How to write excellent Dockerfiles
譯者: Fundebug
相關(guān)文章
docker中配置hosts的方法實(shí)現(xiàn)
本文主要介紹了docker中配置hosts的方法實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04docker 部署prometheus+grafana的過(guò)程詳解
這篇文章主要介紹了docker 部署prometheus+grafana的過(guò)程,本文通過(guò)示例圖文相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2024-03-03Docker私有倉(cāng)庫(kù)打開2375端口的實(shí)現(xiàn)
本文主要介紹了Docker私有倉(cāng)庫(kù)打開2375端口的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03詳解Docker數(shù)據(jù)管理(數(shù)據(jù)卷&數(shù)據(jù)卷容器)
容器中管理數(shù)據(jù)主要有兩種方式,這篇文章主要介紹了詳解Docker數(shù)據(jù)管理(數(shù)據(jù)卷&數(shù)據(jù)卷容器) ,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10講解使用Docker搭建Java Web運(yùn)行環(huán)境
本篇文章詳細(xì)的介紹了使用Docker搭建Java Web運(yùn)行環(huán)境,想要學(xué)習(xí)docker的同學(xué)可以了解一下。2016-11-11詳解掛載運(yùn)行的docker容器中如何掛載文件系統(tǒng)
這篇文章主要給大家介紹了如何在掛載運(yùn)行中的docker容器中掛載文件系統(tǒng),文中通過(guò)一步步的實(shí)踐過(guò)程介紹的很詳細(xì),相信對(duì)有需要的朋友們來(lái)說(shuō)具有一定的參考借鑒價(jià)值,感興趣的朋友們下面來(lái)一起看看吧。2016-12-12