1. 程式人生 > >【轉】編寫最佳的Dockerfile的實踐

【轉】編寫最佳的Dockerfile的實踐

Dockerfile的語法非常簡單,然而如何加快映象構建速度,如何減少Docker映象的大小卻不是那麼直觀,需要積累實踐經驗。這篇部落格可以幫助你快速掌握編寫Dockerfile的技巧。

目標:

更快的構建速度 更小的Docker映象大小 更少的Docker映象層 充分利用映象快取 增加Dockerfile可讀性 讓Docker容器使用起來更簡單

總結

編寫.dockerignore檔案 容器只執行單個應用 將多個RUN指令合併為一個 基礎映象的標籤不要用latest 每個RUN指令後刪除多餘檔案 選擇合適的基礎映象(alpine版本最好) 設定WORKDIR和CMD 使用ENTRYPOINT (可選) 在entrypoint指令碼中使用exec COPY與ADD優先使用前者 合理調整COPY與RUN的順序 設定預設的環境變數,對映埠和資料卷 使用LABEL設定映象元資料 新增HEALTHCHECK

示例

示例Dockerfile犯了幾乎所有的錯(當然我是故意的)。接下來,我會一步步優化它。假設我們需要使用Docker執行一個Node.js應用,下面就是它的Dockerfile(CMD指令太複雜了,所以我簡化了,它是錯誤的,僅供參考)。

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 

構建映象:

docker build -t wtf .

1. 編寫.dockerignore檔案

構建映象時,Docker需要先準備context ,將所有需要的檔案收集到程序中。預設的context包含Dockerfile目錄中的所有檔案,但是實際上,我們並不需要.git目錄,node_modules目錄等內容。 .dockerignore 的作用和語法類似於 .gitignore,可以忽略一些不需要的檔案,這樣可以有效加快映象構建時間,同時減少Docker映象的大小。示例如下:

.git/
node_modules/

2. 容器只執行單個應用

從技術角度講,你可以在Docker容器中執行多個程序。你可以將資料庫,前端,後端,ssh,supervisor都執行在同一個Docker容器中。但是,這會讓你非常痛苦:

非常長的構建時間(修改前端之後,整個後端也需要重新構建) 非常大的映象大小 多個應用的日誌難以處理(不能直接使用stdout,否則多個應用的日誌會混合到一起) 橫向擴充套件時非常浪費資源(不同的應用需要執行的容器數並不相同) 殭屍程序問題 - 你需要選擇合適的init程序 因此,我建議大家為每個應用構建單獨的Docker映象,然後使用 Docker Compose 執行多個Docker容器。

現在,我從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. 將多個RUN指令合併為一個

Docker映象是分層的,下面這些知識點非常重要:

Dockerfile中的每個指令都會建立一個新的映象層。 映象層將被快取和複用 當Dockerfile的指令修改了,複製的檔案變化了,或者構建映象時指定的變數不同了,對應的映象層快取就會失效 某一層的映象快取失效之後,它之後的映象層快取都會失效 映象層是不可變的,如果我們再某一層中新增一個檔案,然後在下一層中刪除它,則映象中依然會包含該檔案(只是這個檔案在Docker容器中不可見了)。 Docker映象類似於洋蔥。它們都有很多層。為了修改內層,則需要將外面的層都刪掉。記住這一點的話,其他內容就很好理解了。

現在,我們將所有的RUN指令合併為一個。同時把apt-get upgrade刪除,因為它會使得映象構建非常不確定(我們只需要依賴基礎映象的更新就好了)

FROM ubuntu
 
ADD . /app
 
RUN apt-get update \ 
  && apt-get install -y nodejs \
  && cd /app \
  && npm install
 
CMD npm start 

記住一點,我們只能將變化頻率一樣的指令合併在一起。將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. 基礎映象的標籤不要用latest

當映象沒有指定標籤時,將預設使用latest 標籤。因此, FROM ubuntu 指令等同於FROM ubuntu:latest。當時,當映象更新時,latest標籤會指向不同的映象,這時構建映象有可能失敗。如果你的確需要使用最新版的基礎映象,可以使用latest標籤,否則的話,最好指定確定的映象標籤。

示例Dockerfile應該使用16.04作為標籤。

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. 每個RUN指令後刪除多餘檔案

假設我們更新了apt-get源,下載,解壓並安裝了一些軟體包,它們都儲存在/var/lib/apt/lists/目錄中。但是,執行應用時Docker映象中並不需要這些檔案。我們最好將它們刪除,因為它會使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. 選擇合適的基礎映象(alpine版本最好)

在示例中,我們選擇了ubuntu作為基礎映象。但是我們只需要執行node程式,有必要使用一個通用的基礎映象嗎?node映象應該是更好的選擇。

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是一個極小化的Linux發行版,只有4MB,這讓它非常適合作為基礎映象。

FROM node:7-alpine
 
ADD . /app
RUN cd /app && npm install
 
CMD npm start 

apk是Alpine的包管理工具。它與apt-get有些不同,但是非常容易上手。另外,它還有一些非常有用的特性,比如no-cache和 --virtual選項,它們都可以幫助我們減少映象的大小。

7. 設定WORKDIR和 CMD

WORKDIR指令可以設定預設目錄,也就是執行RUN / CMD / ENTRYPOINT指令的地方。

CMD指令可以設定容器建立是執行的預設命令。另外,你應該講命令寫在一個數組中,陣列中每個元素為命令的每個單詞(參考官方文件)。

FROM node:7-alpine
 
WORKDIR /app
ADD . /app
RUN npm install
 
CMD ["npm", "start"] 

8. 使用ENTRYPOINT (可選)

ENTRYPOINT指令並不是必須的,因為它會增加複雜度。ENTRYPOINT是一個指令碼,它會預設執行,並且將指定的命令錯誤其引數。它通常用於構建可執行的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"] 

可以使用如下命令執行該映象:

# 執行開發版本
docker run our-app dev 
 
# 執行生產版本
docker run our-app start 
 
# 執行bash
docker run -it our-app /bin/bash

9. 在entrypoint指令碼中使用exec

在前文的entrypoint指令碼中,我使用了exec命令執行node應用。不使用exec的話,我們則不能順利地關閉容器,因為SIGTERM訊號會被bash指令碼程序吞沒。exec命令啟動的程序可以取代指令碼程序,因此所有的訊號都會正常工作。

10. COPY與ADD優先使用前者

COPY指令非常簡單,僅用於將檔案拷貝到映象中。ADD相對來講複雜一些,可以用於下載遠端檔案以及解壓壓縮包(參考官方文件)。

FROM node:7-alpine
 
WORKDIR /app
 
COPY . /app
RUN npm install
 
ENTRYPOINT ["./entrypoint.sh"] 
CMD ["start"] 

11. 合理調整COPY與RUN的順序

我們應該把變化最少的部分放在Dockerfile的前面,這樣可以充分利用映象快取。

示例中,原始碼會經常變化,則每次構建映象時都需要重新安裝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. 設定預設的環境變數,對映埠和資料卷

執行Docker容器時很可能需要一些環境變數。在Dockerfile設定預設的環境變數是一種很好的方式。另外,我們應該在Dockerfile中設定對映埠和資料卷。示例如下:

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指令指定的環境變數在容器中可以使用。如果你只是需要指定構建映象時的變數,你可以使用ARG指令。

13. 使用LABEL設定映象元資料

使用LABEL指令,可以為映象設定元資料,例如映象建立者或者映象說明。舊版的Dockerfile語法使用MAINTAINER指令指定映象建立者,但是它已經被棄用了。有時,一些外部程式需要用到映象的元資料,例如nvidia-docker需要用到com.nvidia.volumes.needed。示例如下:

FROM node:7-alpine 
LABEL maintainer "[email protected]"

14. 新增HEALTHCHECK

執行容器時,可以指定–restart always選項。這樣的話,容器崩潰時,Docker守護程序(docker daemon)會重啟容器。對於需要長時間執行的容器,這個選項非常有用。但是,如果容器的確在執行,但是不可(陷入死迴圈,配置錯誤)用怎麼辦?使用HEALTHCHECK指令可以讓Docker週期性的檢查容器的健康狀況。我們只需要指定一個命令,如果一切正常的話返回0,否則返回1。對HEALTHCHECK感興趣的話,可以參考這篇部落格。示例如下:

FROM node:7-alpine 
LABEL maintainer "[email protected]"
 
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"] 

當請求失敗時,curl --fail 命令返回非0狀態。

原文: How to write excellent Dockerfiles 譯者: Fundebug