[Docker]寫 Dockerfile 的最佳實踐理論

HitTwice發表於2018-06-29

  本文作者:farwish
  原文連結:https://www.cnblogs.com/farwish/p/9231228.html


  
·指導方針

  建立短暫的容器

  意思是 container 可以停止和銷燬,接著以最小化啟動和配置進行重新構建和替換。

  理解構建的上下文

  使用 docker build ,當前工作環境稱為 構建的上下文,預設 Dockerfile 是在同級目錄找,可透過 -f 指定 Dockerfile。

  無論 Dockerfile 實際在哪裡,當前目錄的所有遞迴的檔案和目錄的內容被髮送到 docker daemon 作為構建的上下文。

  (無意中包含的不必要檔案會增加 image 大小,增加 build/pull/push 時間和 container 執行時大小)

  上下文內容支援本地 PATH 和遠端 URL,docker build [OPTIONS] PATH | URL | -

  17.05開始支援用 stdin 管道化 Dockerfile 的內容

  docker build -t foo:v1 . -f-<<<EOF

  FROM ubuntu:16.04

  RUN echo "hello"

  COPY./copy-files

  EOF

  使用 .dockerignore 排除內容進入構建上下文

  示例 

  使用多級構建

  不需要努力去減少中間層數量和檔案,從變化少的層到經常變化的層來排序(這樣可以保證複用到構建歷史快取)

  不同層的順序安排:安裝工具 -> 安裝庫依賴 -> 生成應用

  不安裝不必要的包

  解耦應用

  每一個 container 應該只關心一件事。解耦應用到多個容器可以讓水平擴充套件更容易和重複使用容器。比如 web 技術棧的分為 應用/資料庫/快取 三個不同的容器。

  最小化層的數量

  在 17.05 及更高版本中,透過 multi-stage 多級構建減少了這個限制。

  給多行引數排序

  幫助避免包重複和更容易修改,更易讀。

  Run apt-get update && apt-get install -y \

  bzr \

  cvs \

  git \

  mercurial

  利用構建快取

  如果不想在構建中使用快取的,給 docker build 命令加 --no-cache=true 選項。

  ·Dockerfile 指令

  FROM - 只要有可能,使用當前官方倉庫的作為基礎 image。

  LABEL - 透過object幫助組織image,每行以LABEL開頭,有一個或多個 key-value 對。https://docs.docker.com/config/labels-custom-metadata/

  RUN - 把長且複雜的 RUN 語句用反斜線分割成多行,保持 Dockerfile 更可讀,可理解,可維護。

  把 update 和 install 放在一行,保證 Dockerfile 安裝最近的包版本,如下:

  RUN apt-get update && apt-get install -y \

  package-foo \

  package-bar

  APT-GET - 在 apt-get 應用中可能是最常使用的用於 RUN 的命令。因為它可以安裝包,RUN apt-get 有幾個陷阱需要提防。

  不要使用 RUN apt-get dist-upgrade 和 dist-upgrade。因為許多來自父級 image 的基礎的包不能在沒有許可權的 container 中升級。

  如果父級 image 中的一個 package 過時了,聯絡它的維護者。

  如果你知道有一個特定的包 foo 需要升級,使用 apt-get install -y foo 來自動更新。

  總是把 RUN apt-get update 和 apt-get install 合併為一條 RUN 語句,確保無干涉的安裝最新的包版本。

  RUN apt-get update && apt-get install -y \

  package-foo \

  package-bar \

  package-baz=1.3.*

  && rm -rf /var/lib/apt/lists/* # 透過移除 apt cache 減小 image 尺寸

  單獨在一行 RUN 語句中使用 apt-get update 會引起快取問題,隨後的 apt-get install 指令失敗,這叫做’ cache busting ’,同樣可以透過指定包版本獲取 cache-busting,這叫做版本固定,這可以避免由包變化而引起的意外失敗。

  官方 debian 和 ubuntu 的 image 會自動執行 *apt-get clean*,所以明確呼叫不是必需的。

  使用管道 - 如果想讓管道連線的命令在任何階段遇到錯誤時就失敗,在命令前追加 set -o pipefail && 來保證遇到未期的錯誤時阻止構建。

  例如:RUN set -o pipefail && wget -O - | wc -l > /number

  不是所有的 shell 都支援 -o pipefail , 基於 debian 的 image 需要指定 bash

  例如:RUN [ “/bin/bash”, “-c”, “set -o pipefail && wget -O - | wc -l > /number" ]

  CMD - 該指令用於執行 image 內的軟體,隨同任何引數。

  格式形式為 CMD ["command", "param1", "param2"],這個形式的指令推薦用在任何基於服務的 image。

  在大多數其他案例中,CMD 應該給一個互動式的shell,比如 CMD ["php", "-a"],意味著執行 docker run -it php,你會有一個可用的shell。

  CMD 很少以 CMD ["param", "param"] 的方式與 ENTRYPOINT 協同,除非你和使用者已經非常熟悉 ENTRYPOINT 是如何工作的。

  EXPOSE - 該指令指示 container 在哪一個埠監聽用於連線。

  因此,應該使用常見傳統的埠用於應用軟體。例如 包含 Apache 的 image 將使用 EXPOSE 80,而包含 MongoDB 的 image 將使用 EXPOSE 27017 等等。

  用於外部訪問,使用者可以執行 docker run 帶上一個標記來標識對映指定埠到他們選擇的埠。

  對於容器連線,Docker 為從接收容器返回到源的路徑提供環境變數。(如,MYSQL_PORT_3306_TCP)

  ENV - 為了使新軟體更容易執行,可以使用 ENV 來更新容器中安裝軟體的 PATH 環境變數。

  例如:ENV PATH /usr/local/nginx/bin:$PATH 保證 CMD ["nginx"] 能執行。

  ENV 指令 在 為你希望容器化的服務提供所需的環境變數 上同樣有用,例如 Postgres 的 PGDATA。

  每個 ENV 行建立一個新的中間層,就像 RUN 命令。這意味著即使在之後的層 unset 這個環境變數,它仍然存在於這個層,並且它的值可以被列印。測試如下:

  FROM alpine

  ENV ADMIN_USER="mark"

  RUN echo $ADMIN_USER > ./mark

  RUN unset ADMIN_USER

  CMD sh

  $ docker run --rm -it test sh echo $ADMIN_USER

  要阻止這種情況,真正 unset 環境變數,使用 RUN 指令執行 shell 命令,在一個獨立的層中 set, use, unset 變數。

  使用 ; 或 && 來分割命令,使用 && 只要一個命令失敗,docker build 也失敗。

  FROM alpine

  ENV export ADMIN_USER="mark" \

  && echo $ADMIN_USER > ./mark \

  && unset ADMIN_USER

  CMD sh

  $ docker run --rm -it test sh echo $ADMIN_USER

  ADD 或 COPY - 儘管兩者功能相似,一般來講,首選 COPY。

  因為 COPY 比 ADD 更易懂。COPY 只支援從本地檔案到 container 的基本複製,而 ADD 有一些不明顯的特性(如,本地 tar包 自動解壓和支援遠端 URL)

  因此 ADD 的最佳使用是本地 tar 檔案在 image 中的自動解壓,如,ADD rootfs.tar.xz / .

  如果 Dockerfile 有多個步驟使用了上下文中的不同檔案,單獨的複製它們,而不是一次複製所有。這確保每一步的構建快取在檔案發生改變時是失效的,如:

  COPY requirements.txt /tmp

  RUN pip install --requirement /tmp/requirements.txt

  COPY . /tmp/

  要讓 RUN 步驟導致更少的快取失效,那麼把 COPY . /tmp/ 放到其前面。

  因為 image 大小問題,使用 ADD 從遠端地址拉取包是極不鼓勵的;你應該使用 curl 或 wget 代替。這種方式你可以在解壓後把不需要的檔案刪除,不需要把其它層加到你的 image 中。

  避免做的是:

  ADD /usr/src/things/

  RUN tar -zJf /usr/src/things/big.tar.xz -C /usr/src/things

  RUN make -C /usr/src/things all

  代替的做法是:

  RUN mkdir -p /usr/src/things \

  && curl -SL \ 

  |tar -xJC /usr/src/things \

  && make -C /usr/src/things all

  對於不需要 ADD 自動解壓能力的檔案和目錄,你應該總是使用 COPY。

  ENTRYPOINT - 最好的使用點是設定 image 的主命令,允許 image 像命令一樣執行(接著使用 CMD 做為預設標記)。

  ENTRYPOINT ["s3cmd"]

  CMD ["--help"]

  現在 image 可以像這樣顯示命令的幫助資訊:$ docker run s3cmd

  或者使用正確的引數來執行一個命令:$ docker run s3cmd ls s3://mybucket

  這是有用的,因為 image 名稱可以兩次作為到如上命令二進位制的引用。

  ENTRYPOINT 指令同樣可以用來和一個幫助指令碼結合,允許它做和命令方式一樣的事,儘管需要多進行一步 - 寫指令碼,以下是 Postgres 官方 image 的 ENTRYPOINT 使用的指令碼:

  #!/bin/bash

  set -e

  if [ "$1" = 'postgres' ]; then

  chown -R postgres "$PGDATA"

  if [ -z "$(ls -A "$PGDATA")" ]; then

  gosu postgres initdb

  fi

  fi

  exec "$@"

  把幫助指令碼複製到 container 中,並在 container 啟動時透過 ENTRYPOINT 執行:

  COPY ./docker-entrypoint.sh /

  ENTRYPOINT ["/docker-entrypoint.sh"]

  CMD ["postgres"]

  幫助指令碼允許命令有多個引數可以互動,比如:

  $ docker run postgres

  $ docker run postgres postgres --help

  $ docker run --rm -it postgres bash

  VOLUME - 指令應該用於暴露任何資料庫儲存區域,配置區域,或 docker 容器建立的檔案/目錄。

  強烈建議你使用 VOLUMN 在 image 中易變的或使用者維護的部分。

  USER - 如果一個服務不需要許可權可以執行,使用 USER 切換為非 root 使用者。

  透過在 Dockerfile 中建立使用者和組開始開始:RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres

  image 中的使用者和組被分配一個非確定的 UID/GID,下次 image 重建時重新分配。所以如果 UID/GID 至關重要,你應該分配一個準確的。

  (由於Go archive/tar 包中未解決的bug,在docker 容器中建立相當長的UID會耗盡磁碟,因為容器層中的 /var/log/faillog 填充了NULL字元,權宜措施是傳遞 --no-log-init 給 useradd)

  避免使用 sudo,如果你確實需要像 sudo 一樣的功能,例如像 root 一樣初始化守護程式但以非 root 使用者執行,考慮使用 gosu。

  為了減少層次和複雜性,避免頻繁的切換使用者。

  WORKDIR - 為了清晰和可靠,你應該總是為 WORKDIR 使用絕對路徑。

  同樣,你應該使用 WORKDIR 而不是 RUN cd ... && do-something ,更難度閱讀、排除問題和維護。

  ONBUILD - 命令在當前 Dockerfile 構建完畢執行。

  ONBUILD 在當前 FROM image 派生的任何子 image 中執行。可以認為 ONBUILD 命令是父 Dockerfile 給子 Dockerfile 的指令。

  Docker 構建在任何子 Dockerfile 中的命令前執行 ONBUILD。

  ONBUILD 對於從給定的 image 構建 image 時有用。例如,你想為一個語言棧的 包含該語言寫的任意使用者軟體到 Dockerfile 中的 image 使用 ONBUILD。

  從 ONBUILD 構建的 image 應該有一個分割 tag,例如,ruby:1.9-onbuild 或 ruby-2.0-onbuild

  當把 ADD 或 COPY 放到 ONBUILD 時要小心。如果新構建的上下文缺少被新增的資源,onbuild 構建 image 會災難性失敗。透過如上新增分割的 tag 來減輕這種影響。

  Guides: https://docs.docker.com/develop/develop-images/dockerfile_best-practices

  Link: https://www.cnblogs.com/farwish/p/9231228.html

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31473948/viewspace-2157061/,如需轉載,請註明出處,否則將追究法律責任。

相關文章