如何編寫最佳的Dockerfile

MeghanWickh 7年前發布 | 288K 次閱讀 Docker

譯者按:Dockerfile的語法非常簡單,然而如何加快鏡像構建速度,如何減少Docker鏡像的大小卻不是那么直觀,需要積累實踐經驗。這篇博客可以幫助你快速掌握編寫Dockerfile的技巧。

為了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原作者所有,翻譯僅用于學習。

我已經使用Docker有一段時間了,其中編寫Dockerfile是非常重要的一部分工作。在這篇博客中,我打算分享一些建議,幫助大家編寫更好的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

RUNapt-get update
RUNapt-get upgrade -y
RUNapt-get install -y nodejs ssh mysql
RUNcd /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
CMDmysql & 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

RUNapt-get update
RUNapt-get upgrade -y

# we should remove ssh and mysql, and use
# separate container for database
RUNapt-get install -y nodejs # ssh mysql
RUNcd /app && npm install

CMDnpm start

3. 將多個RUN指令合并為一個

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

  • Dockerfile中的每個指令都會創建一個新的鏡像層。
  • 鏡像層將被緩存和復用
  • 當Dockerfile的指令修改了,復制的文件變化了,或者構建鏡像時指定的變量不同了,對應的鏡像層緩存就會失效
  • 某一層的鏡像緩存失效之后,它之后的鏡像層緩存都會失效
  • 鏡像層是不可變的,如果我們再某一層中添加一個文件,然后在下一層中刪除它,則鏡像中依然會包含該文件(只是這個文件在Docker容器中不可見了)。

Docker鏡像類似于洋蔥。它們都有很多層。為了修改內層,則需要將外面的層都刪掉。記住這一點的話,其他內容就很好理解了。

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

FROM ubuntu

ADD. /app

RUNapt-get update \
 && apt-get install -y nodejs \
 && cd /app \
 && npm install

CMDnpm start

記住一點,我們只能將變化頻率一樣的指令合并在一起。將node.js安裝與npm模塊安裝放在一起的話,則每次修改源代碼,都需要重新安裝node.js,這顯然不合適。因此,正確的寫法是這樣的:

FROM ubuntu

RUNapt-get update && apt-get install -y nodejs
ADD. /app
RUNcd /app && npm install

CMDnpm start

4. 基礎鏡像的標簽不要用latest

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

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

FROM ubuntu:16.04 # it's that easy!

RUNapt-get update && apt-get install -y nodejs
ADD. /app
RUNcd /app && npm install

CMDnpm start

5. 每個RUN指令后刪除多余文件

假設我們更新了apt-get源,下載,解壓并安裝了一些軟件包,它們都保存在 /var/lib/apt/lists/ 目錄中。但是,運行應用時Docker鏡像中并不需要這些文件。我們最好將它們刪除,因為它會使Docker鏡像變大。

示例Dockerfile中,我們可以刪除 /var/lib/apt/lists/ 目錄中的文件(它們是由apt-get update生成的)。

FROM ubuntu:16.04

RUNapt-get update \
 && apt-get install -y nodejs \
 # added lines
 && rm -rf /var/lib/apt/lists/*

ADD. /app
RUNcd /app && npm install

CMDnpm start

6. 選擇合適的基礎鏡像(alpine版本最好)

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

FROM node

ADD. /app
# we don't need to install node
# anymore and use apt-get
RUNcd /app && npm install

CMDnpm start

更好的選擇是alpine版本的 node 鏡像。alpine是一個極小化的Linux發行版,只有4MB,這讓它非常適合作為基礎鏡像。

FROM node:7-alpine

ADD. /app
RUNcd /app && npm install

CMDnpm start

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

7. 設置WORKDIR和 CMD

WORKDIR 指令可以設置默認目錄,也就是運行 RUN / CMD / ENTRYPOINT 指令的地方。

CMD 指令可以設置容器創建是執行的默認命令。另外,你應該講命令寫在一個數組中,數組中每個元素為命令的每個單詞(參考 官方文檔 )。

FROM node:7-alpine

WORKDIR/app
ADD. /app
RUNnpm 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
RUNnpm 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
RUNnpm install

ENTRYPOINT["./entrypoint.sh"]
CMD["start"]

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

我們應該 把變化最少的部分放在Dockerfile的前面 ,這樣可以充分利用鏡像緩存。

示例中,源代碼會經常變化,則每次構建鏡像時都需要重新安裝NPM模塊,這顯然不是我們希望看到的。因此我們可以先拷貝 package.json ,然后安裝NPM模塊,最后才拷貝其余的源代碼。這樣的話,即使源代碼變化,也不需要重新安裝NPM模塊。

FROM node:7-alpine

WORKDIR/app

COPYpackage.json /app
RUNnpm install
COPY. /app

ENTRYPOINT["./entrypoint.sh"]
CMD["start"]

12. 設置默認的環境變量,映射端口和數據卷

運行Docker容器時很可能需要一些環境變量。在Dockerfile設置默認的環境變量是一種很好的方式。另外,我們應該在Dockerfile中設置映射端口和數據卷。示例如下:

FROM node:7-alpine

ENV PROJECT_DIR=/app

WORKDIR$PROJECT_DIR

COPYpackage.json $PROJECT_DIR
RUNnpm 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 
LABELmaintainer "jakub.skalecki@example.com"
...

14. 添加HEALTHCHECK

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

FROM node:7-alpine 
LABELmaintainer "jakub.skalecki@example.com"

ENV PROJECT_DIR=/app 
WORKDIR$PROJECT_DIR

COPYpackage.json $PROJECT_DIR
RUNnpm install
COPY. $PROJECT_DIR

ENV MEDIA_DIR=/media \ 
 NODE_ENV=production \
 APP_PORT=3000

VOLUME$MEDIA_DIR
EXPOSE $APP_PORT 
HEALTHCHECKCMD curl --fail http://localhost:$APP_PORT || exit 1

ENTRYPOINT["./entrypoint.sh"]
CMD["start"]

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

來自:https://kiwenlau.com/2017/05/15/write-excellent-dockerfile/

 

 本文由用戶 MeghanWickh 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!