Dockerfile 最佳實踐

雖然 Dockerfile 簡化了鏡像構建的過程,而且把這個過程能夠進行版本控制,可是不正當的 Dockerfile 使用也會致使不少問題:java

  • docker 鏡像太大。若是你常用鏡像或者構建鏡像,必定會遇到那種很大的鏡像,甚至有些能達到 數G
  • docker 鏡像的構建時間過長。每一個 build 都會耗費很長時間,對於須要常常構建鏡像(好比單元測試)的地方這多是個大問題
  • 重複勞動。屢次鏡像構建之間大部份內容都是徹底同樣並且重複的,可是每次都要作一遍,浪費時間和資源

通常指導方針和建議

容器應該是短暫的

容器模型是進程而不是機器,不須要開機初始化。在須要時運行,不須要時中止,可以刪除後重建,而且配置和啓動的最小化。node

使用.dockerignore文件

在 docker build 的時候,忽略部分無用的文件和目錄能夠提升構建的速度。好比.git目錄。dockerignore的定義相似gitignore。具體使用方式能夠參考連接python

避免安裝沒必要要的安裝包

爲了減小鏡像的複雜性、鏡像大小和構建時間,應該避免安裝無用的包。mysql

每一個容器只運行一個進程

一個容器只運行一個進程。容器起到了隔離應用隔離數據的做用,不一樣的應用運行在不一樣的容器讓集羣的縱向擴展以及容器的複用都變的更加簡單。須要多個應用交互時請使用 link 命令進行組合或者使用docker-compose。nginx

最小化層數

須要掌握好Dockerfile的可讀性和鏡像層數之間的平衡。不推薦使用過多的鏡像層。git

多行命令按字母排序

命令行按字母順序排序有助於避免重複執行和提升Dockerfile可讀性。apt-get update 應與 apt-get install 組合,換行使用反斜槓(\)。例如:github

RUN apt-get update && apt-get install -y \
  bzr \
  cvs \
  git \
  mercurial \
  subversion

構建緩存

Dockerfile的每條指令都會將結果提交爲新的鏡像。下一條指令基於上一條指令的鏡像進行構建。若是一個鏡像擁有相同的父鏡像和指令(除了 ADD ),Docker將會使用鏡像而不是執行該指令,即緩存。golang

所以,爲了有效的利用緩存,儘可能保持Dockerfile一致,而且將不變的放在前面而常常改變放在末尾。web

如不但願使用緩存,在執行 docker build 的時候加上參數 --no-cache=true 。sql

Docker匹配鏡像決定是否使用緩存的規則以下:

  • 從緩存中存在的基礎鏡像開始,比較全部子鏡像,檢查它們構建的指令是否和當前的是否徹底一致。若是不一致則緩存不匹配。
  • 多數狀況中,比較Dockerfile中的指令是足夠的。然而,特定的指令須要作更多的判斷。
  • ADD COPY 指令中,將要添加到鏡像中的文件也要被檢查。一般是檢查文件的校驗和(checksum)。
  • 緩存匹配檢查並不檢查容器中的文件。例如,當使用 RUN apt-get -y update 命令更新了容器中的文件,並不會被緩存檢查策略做爲緩存匹配的依據。

時區

官方Image 使用的時區基本上都是標準的 UTC 時間,若是容器想使用中國標準時間,基於Debian的系統在Dockerfile中加入

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" >> /etc/timezone

基於Centos的系統在Dockerfile中加入

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

修改默認源

有時候你可能感受官方的源更新或者安裝軟件比較慢,能夠在Dockerfile修改官方默認源,例如alpine想使用阿里的源能夠在Dockerfile中加入:

RUN echo -e "http://mirrors.aliyun.com/alpine/v3.5/main\nhttp://mirrors.aliyun.com/alpine/v3.5/community" > /etc/apk/repositories

Dockerfile指令

FROM

推薦使用官方倉庫中的鏡像做爲基礎鏡像。

RUN

把複雜的或過長的 RUN 語句寫成以 \ 結尾的多行的形式,以提升可讀性和可維護性。

apt-get update 和 apt-get install 一塊兒執行,不然 apt-get install 會出現異常。

避免運行 apt-get upgrade 或 dist-upgrade ,在無特權的容器中,不少 必要 的包不能正常升級。若是基礎鏡像過期了,應當聯繫維護者。 推薦 apt-get update && apt-get install -y package-a package-b 這種方式,先更新,以後安裝最新的軟件包。

RUN apt-get update && apt-get install -y \
    aufs-tools \
    automake \
    build-essential \
    curl \
    dpkg-sig \
    libcap-dev \
    libsqlite3-dev \
    mercurial \
    reprepro \
    ruby1.9.1 \
    ruby1.9.1-dev \
    s3cmd=1.1.* \
 && rm -rf /var/lib/apt/lists/*

此外,你能夠經過移除/var/lib/apt/lists減小鏡像大小。

注意:官方的Ubuntu和Debian會自動運行apt-get clean,因此不須要顯式的調用

CMD

推薦使用 CMD ["executable","param1","param2"] 這樣的格式。若是鏡像是用來運行服務,須要使用 CMD["apache2","-DFOREGROUND"] ,這種格式的指令適用於任何服務性質的鏡像。

ENTRYPOINT

ENTRYPOINT 應該用於 鏡像的主命令,並使用 CMD 做爲默認設置,以 s3cmd 爲例:

ENTRYPOINT ["s3cmd"]
CMD ["--help"]

獲取幫助:

docker run s3cmd

或者執行命令:

docker run s3cmd ls s3://mybucket

這在鏡像名與程序重名時很是有用。

ENTRYPOINT 也能夠啓動自定義腳本: 定義腳本:

#!/bin/bash
set -e

if [ "$1" = 'postgres' ]; then
    chown -R postgres "$PGDATA"

    if [ -z "$(ls -A "$PGDATA")" ]; then
        gosu postgres initdb
    fi

    exec gosu postgres "$@"
fi

exec "$@"

注意:這段腳本使用了exec命令以確保最終應用程序在容器內啓動的PID爲1。這段腳本容許容器接收Unix signals。

COPY ./docker-entrypoint.sh /
ENTRYPOINT ["/docker-entrypoint.sh"]

這段腳本爲用戶提供了多種和 Postgres 交互的途徑:

你能夠簡單地啓動 Postgres:

docker run postgres。

或者運行 postgres 並傳入參數:

docker run postgres postgres --help。

你甚至能夠從鏡像中啓動一個徹底不一樣的程序,好比 Bash:

docker run --rm -it postgres bash

EXPOSE

應該儘量地使用默認端口。例如Apache web服務使用EXPOSE 80,MongoDB使用EXPOSE 27017。

ENV

  • 可使用ENV更新PATH環境變量。例如,ENV PATH /usr/local/nginx/bin:$PATH能夠確保CMD [「nginx」]正常運行。
  • ENV還能夠提供程序所須要的環境變量,例如Postgres的 PGDATA。
  • ENV能夠設置版本等信息。使版本信息更易於維護。
ENV PG_MAJOR 9.3
ENV PG_VERSION 9.3.4
RUN curl -SL http://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgress && …
ENV PATH /usr/local/postgres-$PG_MAJOR/bin:$PATH

ADD or COPY

雖然 ADD 與 COPY 功能相似,但推薦使用 COPY 。 COPY 只支持基本的文件拷貝功能,更加的可控。而 ADD 具備更多特定,好比tar文件自動提取,支持URL。 一般須要提取tarball中的文件到容器的時候纔會用到 ADD 。

若是在Dockerfile中使用多個文件,每一個文件應使用單獨的 COPY 指令。這樣,只有出現文件變化的指令纔會不使用緩存。

爲了控制鏡像的大小,不建議使用 ADD 指令獲取URL文件。正確的作法是在 RUN 指令中使用 wget 或 curl 來獲取文件,而且在文件不須要的時候刪除文件。

RUN mkdir -p /usr/src/things \
    && curl -SL http://example.com/big.tar.gz \
    | tar -xJC /usr/src/things \
    && make -C /usr/src/things all

VOLUME

VOLUME 一般用做數據卷,對於任何可變的文件,包括數據庫文件、代碼庫、或者容器所建立的文件/目錄等都應該使用 VOLUME 掛載。

USER

若是服務不須要特權來運行,使用 USER 指令切換到非root用戶。使用 **RUN groupadd -r mysql && useradd -r -g mysql mysql **以後用 USER mysql 切換用戶

要避免使用 sudo 來提高權限,由於它帶來的問題遠比它能解決的問題要多。若是你確實須要這樣的特性,那麼能夠選擇使用 gosu

最後,不要反覆的切換用戶。減小沒必要要的layers。

WORKDIR

爲了清晰和可維護性,應該使用WORKDIR來定義工做路徑。推薦使用WORKDIR來代替RUN cd … && do-something 這樣的指令。

ONBUILD

Dockerfile 中的其它指令都是爲了定製當前鏡像而準備的,惟有 ONBUILD 是爲了幫助別人定製本身而準備的。

ONBUILD指令用來設置一些觸發的指令,用於在當該鏡像被做爲基礎鏡像來建立其餘鏡像時(也就是Dockerfile中的FROM爲當前鏡像時)執行一些操做,ONBUILD中定義的指令會在用於生成其餘鏡像的Dockerfile文件的FROM指令以後被執行,上述介紹的任何一個指令均可以用於ONBUILD指令,能夠用來執行一些由於環境而變化的操做,使鏡像更加通用。

注意:

  • ONBUILD中定義的指令在當前鏡像的build中不會被執行。
  • 能夠經過查看docker inspect <image>命令執行結果的OnBuild鍵來查看某個鏡像ONBUILD指令定義的內容。
  • ONBUILD中定義的指令會當作引用該鏡像的Dockerfile文件的FROM指令的一部分來執行,執行順序會按ONBUILD定義的前後順序執行,若是ONBUILD中定義的任何一個指令運行失敗,則會使FROM指令中斷並致使整個build失敗,當全部的ONBUILD中定義的指令成功完成後,會按正常順序繼續執行build。
  • ONBUILD中定義的指令不會繼承到當前引用的鏡像中,也就是當引用ONBUILD的鏡像建立完成後將會清除全部引用的ONBUILD指令。
  • ONBUILD指令不容許嵌套,例如ONBUILD ONBUILD ADD . /data是不容許的。
  • ONBUILD指令不會執行其定義的FROM或MAINTAINER指令。

例如,Dockerfile使用以下的內容建立了鏡像 image-A :

[...]
ONBUILD ADD . /app/src
ONBUILD RUN /usr/local/bin/python-build --dir /app/src
[...]

若是基於 image-A 建立新的鏡像時,新的Dockerfile中使用FROM image-A指定基礎鏡像時,會自動執行ONBUILD指令內容,等價於在後面添加了兩條指令。

FROM image-A
#Automatically run the following
ADD . /app/src
RUN /usr/local/bin/python-build --dir /app/src

使用場景

Node.js

假設咱們要製做 Node.js 所寫的應用的鏡像。咱們都知道 Node.js 使用 npm 進行包管理,全部依賴、配置、啓動信息等會放到 package.json 文件裏。在拿到程序代碼後,須要先進行 npm install 才能夠得到全部須要的依賴。而後就能夠經過 npm start 來啓動應用。所以,通常來講會這樣寫 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/
CMD [ "npm", "start" ]

把這個 Dockerfile 放到 Node.js 項目的根目錄,構建好鏡像後,就能夠直接拿來啓動容器運行。可是若是咱們還有第二個 Node.js 項目也差很少呢?好吧,那就再把這個 Dockerfile 複製到第二個項目裏。那若是有第三個項目呢?再複製麼?文件的副本越多,版本控制就越困難,讓咱們繼續看這樣的場景維護的問題。

若是第一個 Node.js 項目在開發過程當中,發現這個 Dockerfile 裏存在問題,好比敲錯字了、或者須要安裝額外的包,而後開發人員修復了這個 Dockerfile,再次構建,問題解決。第一個項目沒問題了,可是第二個項目呢?雖然最初 Dockerfile 是複製、粘貼自第一個項目的,可是並不會由於第一個項目修復了他們的 Dockerfile,而第二個項目的 Dockerfile 就會被自動修復。

那麼咱們可不能夠作一個基礎鏡像,而後各個項目使用這個基礎鏡像呢?這樣基礎鏡像更新,各個項目不用同步 Dockerfile 的變化,從新構建後就繼承了基礎鏡像的更新?好吧,能夠,讓咱們看看這樣的結果。那麼上面的這個 Dockerfile 就會變爲:

FROM node:slim
RUN mkdir /app
WORKDIR /app
CMD [ "npm", "start" ]

這裏咱們把項目相關的構建指令拿出來,放到子項目裏去。假設這個基礎鏡像的名字爲 my-node 的話,各個項目內的本身的 Dockerfile 就變爲:

FROM my-node
COPY ./package.json /app
RUN [ "npm", "install" ]
COPY . /app/

基礎鏡像變化後,各個項目都用這個 Dockerfile 從新構建鏡像,會繼承基礎鏡像的更新。

那麼,問題解決了麼?沒有。準確說,只解決了一半。若是這個 Dockerfile 裏面有些東西須要調整呢?好比 npm install 須要統一加一些參數,那怎麼辦?這一行 RUN 是不可能放入基礎鏡像的,由於涉及到了當前項目的 ./package.json,難道又要一個個修改麼?因此說,這樣製做基礎鏡像,只解決了原來的 Dockerfile 的前4條指令的變化問題,然後面三條指令的變化則徹底沒辦法處理。

ONBUILD 能夠解決這個問題。讓咱們用 ONBUILD 從新寫一下基礎鏡像的 Dockerfile:

FROM node:slim
RUN mkdir /app
WORKDIR /app
ONBUILD COPY ./package.json /app
ONBUILD RUN [ "npm", "install" ]
ONBUILD COPY . /app/
CMD [ "npm", "start" ]

此次咱們回到原始的 Dockerfile,可是此次將項目相關的指令加上 ONBUILD,這樣在構建基礎鏡像的時候,這三行並不會被執行。而後各個項目的 Dockerfile 就變成了簡單地:

FROM my-node

是的,只有這麼一行。當在各個項目目錄中,用這個只有一行的 Dockerfile 構建鏡像時,以前基礎鏡像的那三行 ONBUILD 就會開始執行,成功的將當前項目的代碼複製進鏡像、而且針對本項目執行 npm install,生成應用鏡像。

Maven

相似Java,Go等編譯型項目,可使用ONBUILD指令進行優化Dockerfile。

編寫onbuild Dockerfile以下:

FROM maven:3-jdk-8

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

ONBUILD ADD . /usr/src/app

ONBUILD RUN mvn install

而後全部依賴maven編譯的項目Dockerfile能夠簡化成以下形式:

FROM maven:3.3-jdk-8-onbuild
CMD ["java","-jar","/usr/src/app/target/demo-1.0-SNAPSHOT-jar-with-dependencies.jar"]

官方Dockerfile示例:

相關文章
相關標籤/搜索