如何編寫最佳的Dockerfile

譯者按: Dockerfile的語法很是簡單,然而如何加快鏡像構建速度,如何減小Docker鏡像的大小卻不是那麼直觀,須要積累實踐經驗。這篇博客能夠幫助你快速掌握編寫Dockerfile的技巧。前端

原文: How to write excellent Dockerfilesnode

譯者: Fundebugmysql

爲了保證可讀性,本文采用意譯而非直譯。另外,本文版權歸原做者全部,翻譯僅用於學習linux

我已經使用Docker有一段時間了,其中編寫Dockerfile是很是重要的一部分工做。在這篇博客中,我打算分享一些建議,幫助你們編寫更好的Dockerfile。git

目標:

  • 更快的構建速度
  • 更小的Docker鏡像大小
  • 更少的Docker鏡像層
  • 充分利用鏡像緩存
  • 增長Dockerfile可讀性
  • 讓Docker容器使用起來更簡單

總結

  • 編寫.dockerignore文件github

  • 容器只運行單個應用sql

  • 將多個RUN指令合併爲一個docker

  • 基礎鏡像的標籤不要用latest數據庫

  • 每一個RUN指令後刪除多餘文件npm

  • 選擇合適的基礎鏡像(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 "jakub.skalecki@example.com"  
...

14. 添加HEALTHCHECK

運行容器時,能夠指定--restart always選項。這樣的話,容器崩潰時,Docker守護進程(docker daemon)會重啓容器。對於須要長時間運行的容器,這個選項很是有用。可是,若是容器的確在運行,可是不可(陷入死循環,配置錯誤)用怎麼辦?使用HEALTHCHECK指令可讓Docker週期性的檢查容器的健康情況。咱們只須要指定一個命令,若是一切正常的話返回0,不然返回1。對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"]

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

歡迎加入咱們FundebugDocker技術交流羣: 305097057

版權聲明:

轉載時請註明做者Fundebug以及本文地址:

https://blog.fundebug.com/2017/05/15/write-excellent-dockerfile/

相關文章
相關標籤/搜索