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

 
  • 指導方針
 
建立短暫的容器
 
  意思是 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。 https://docs.docker.com/engine/reference/builder/#from
 
 
    LABEL - 經過object幫助組織image,每行以LABEL開頭,有一個或多個 key-value 對。 https://docs.docker.com/config/labels-custom-metadata/
 
 
    RUN - 把長且複雜的 RUN 語句用反斜線分割成多行,保持 Dockerfile 更可讀,可理解,可維護。 https://docs.docker.com/engine/reference/builder/#run
 
               把 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 - https://some.site | wc -l > /number
 
                      不是全部的 shell 都支持 -o pipefail , 基於 debian 的 image 須要指定 bash
 
                      例如:RUN [ 「/bin/bash」, 「-c」, 「set -o pipefail && wget -O - https://some.site | wc -l > /number" ]
 
 
     CMD - 該指令用於運行 image 內的軟件,隨同任何參數。 https://docs.docker.com/engine/reference/builder/#cmd
 
                格式形式爲 CMD ["command", "param1", "param2"],這個形式的指令推薦用在任何基於服務的 image。
 
                在大多數其餘案例中,CMD 應該給一個交互式的shell,好比 CMD ["php", "-a"],意味着執行 docker run -it php,你會有一個可用的shell。
 
                CMD 不多以 CMD ["param", "param"] 的方式與 ENTRYPOINT 協同,除非你和用戶已經很是熟悉 ENTRYPOINT 是如何工做的。
 
 
    EXPOSE - 該指令指示 container 在哪個端口監聽用於鏈接。 https://docs.docker.com/engine/reference/builder/#expose
 
                      所以,應該使用常見傳統的端口用於應用軟件。例如 包含 Apache 的 image 將使用 EXPOSE 80,而包含 MongoDB 的 image 將使用 EXPOSE 27017 等等。
 
                      用於外部訪問,使用者能夠執行 docker run 帶上一個標記來標識映射指定端口到他們選擇的端口。
 
                      對於容器鏈接,Docker 爲從接收容器返回到源的路徑提供環境變量。(如,MYSQL_PORT_3306_TCP)
    
 
    ENV - 爲了使新軟件更容易運行,可使用 ENV 來更新容器中安裝軟件的 PATH 環境變量。 https://docs.docker.com/engine/reference/builder/#env
 
               例如: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。 https://docs.docker.com/engine/reference/builder/#add |  https://docs.docker.com/engine/reference/builder/#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 http://example.com/big.tar.xz /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 http://example.com/big.tar.xz \
       |  tar -xJC /usr/src/things \
      && make -C /usr/src/things all

 

                              對於不須要 ADD 自動解壓能力的文件和目錄,你應該老是使用 COPY。
 
 
    ENTRYPOINT - 最好的使用點是設置 image 的主命令,容許 image 像命令同樣運行(接着使用 CMD 作爲默認標記)。 https://docs.docker.com/engine/reference/builder/#entrypoint
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 容器建立的文件/目錄。 https://docs.docker.com/engine/reference/builder/#volume
 
                       強烈建議你把 VOLUMN 使用在 image 中易變的或用戶維護的部分。
 
 
    USER - 若是一個服務不須要權限能夠運行,使用 USER 切換爲非 root 用戶。 https://docs.docker.com/engine/reference/builder/#user
 
                 經過在 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。 https://github.com/tianon/gosu
 
                 爲了減小層次和複雜性,避免頻繁的切換用戶。
 
 
    WORKDIR - 爲了清晰和可靠,你應該老是爲 WORKDIR 使用絕對路徑。 https://docs.docker.com/engine/reference/builder/#workdir
 
                         一樣,你應該使用 WORKDIR 而不是 RUN cd ... && do-something ,更難度閱讀和排除問題以及維護。
 
 
    ONBUILD - 命令在當前 Dockerfile 構建完畢執行。 https://docs.docker.com/engine/reference/builder/#onbuild
 
                        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 來減輕這種影響。
 
 
相關文章
相關標籤/搜索