八、Dockerfile介紹和最佳實踐

1、Dockerfile 概念

一、Dockerfile是什麼

Docker 鏡像是一個特殊的文件系統,除了提供容器運行時所需的程序、庫、資源、配置等文件外,還包含了一些爲運行時準備的一些配置參數(如匿名卷、環境變量、用戶等)。鏡像不包含任何動態數據,其內容在構建以後也不會被改變。php

鏡像的定製實際上就是定製每一層所添加的配置、文件。若是咱們能夠把每一層修改、安裝、構建、操做的命令都寫入一個腳本,用這個腳原本構建、定製鏡像,那麼以前說起的沒法重複的問題、鏡像構建透明性的問題、體積的問題就都會解決。這個腳本就是 Dockerfile。python

Dockerfile 是一個文本文件,其內包含了一條條的指令(Instruction),每一條指令構建一層,所以每一條指令的內容,就是描述該層應當如何構建。有了 Dockerfile,當咱們須要定製本身額外的需求時,只需在 Dockerfile 上添加或者修改指令,從新生成 image 便可,省去了敲命令的麻煩。nginx

2、Dockerfile構建方式

Docker經過對Dockerfile中的一系列指令的順序解析實現自動鏡像構建,構建方式:
  1. 經過使用build命令,根據Dockerfie的命令來構建鏡像,默認加載當前目錄下的Dockerfile文件
  2. 經過源代碼路徑的方式,即指定Dockerfile文件位置,好比Git倉庫位置
  3. 經過標準輸入流的方式

經過源代碼路徑方式

  • Dockerfile須要放置在項目的根目錄位置
  • 在構建的時候,Dockerfile client會把整個context打包發送到Docker Server端,而後由server端負責build鏡像,在構建成功後,會刪除context目錄
  • docker build -t {鏡像名字} {項目路徑能夠是相對路徑}

  

經過標準輸入流方式

  • 經過標準輸入流的方式獲取Dockerfile的內容
  • client不會打包上傳context目錄,所以對於一些ADD、COPY等涉及host本地文件複製的操做不可以支持
  • docker build -t {鏡像名字} - < Dockerfile路徑
 

經過build命令

  • 這是最經常使用的方式,docker build -t {鏡像名字} {項目路徑能夠是相對路徑,也能夠是網絡文件}
  • docker build -t="xuequn/nginx:v1" git@github:loveliuli/custom_dockerfile
  • 注意:custom_dockerfile目錄下必須存在Dockerfile文件才行!

3、Dockerfile構建緩存

  • Dockerfile中的每個指令執行完畢後,都會提交爲一個image,這樣保證了指令之間不會有影響
  • Dockerfile會盡量嘗試重用以前已經構建的鏡像
  • 能夠經過在build命令中增長--no-cache的方式來禁用這個cache

 

4、Dockerfile最佳實踐

Docker鏡像由只讀層組成,每一個層都表明一個Dockerfile指令。這些層是堆疊的,每一層都是前一層變化的增量。c++

一、瞭解構建上下文

發出docker build命令時,當前工做目錄稱爲構建上下文。默認狀況下,假定Dockerfile位於此處,但您可使用文件flag(-f)指定其餘位置。不管Dockerfile實際存在的位置如何,當前目錄中的全部文件和目錄的遞歸內容都將做爲構建上下文發送到Docker守護程序。git

構建上下文示例github

爲構建上下文建立一個目錄並cd進入該目錄。將「hello」寫入名爲的文本文件,hello並建立一個cat在其上運行的Dockerfile 。從構建上下文(.)中構建鏡像:golang

mkdir myproject && cd myproject
echo "hello" > hello
echo -e "FROM busybox\nCOPY /hello /\nRUN cat /hello" > Dockerfile
docker build -t helloapp:v1 .

  

把Dockerfile文件移動到dockerfile文件夾,hello文件移動到context文件夾,構建第二個版本(不依賴於上一個版本的緩存)。使用-f 指向Dockerfile並指定構建上下文的目錄:web

mkdir -p dockerfiles context
mv Dockerfile dockerfiles && mv hello context
docker build --no-cache -t helloapp:v2 -f dockerfiles/Dockerfile context

  

二、管道Dockerfile經過stdin

Docker17.05增長了Dockerfile經過stdin使用本地或遠程構建上下文進行管道來構建鏡像的功能。在早期版本中,使用Dockerfilefrom構建鏡像stdin並未發送構建上下文。sql

Docker17.04及更低版本docker

 
   
docker build -t foo -<<EOF
FROM busybox
RUN echo "hello world"
EOF

Docker 17.05及更高版本(本地構建上下文)

 
   
docker build -t foo . -f-<<EOF
FROM busybox
RUN echo "hello world"
COPY . /my-copied-files
EOF

Docker 17.05及更高版本(遠程構建上下文)

 
   
docker build -t foo https://github.com/thajeztah/pgadmin4-docker.git -f-<<EOF
FROM busybox
COPY LICENSE config_local.py /usr/local/lib/python2.7/site-packages/pgadmin4/
EOF 

三、使用.dockerignore

要排除與構建無關的文件(不重構源存儲庫),請使用.dockerignore文件。此文件支持與.gitignore文件相似的排除模式。有關建立一個的信息,請參閱 .dockerignore文件

四、使用多階段構建

多階段構建(在Docker 17.05或更高版本中)容許您大幅減少最終鏡像的大小,而沒必要費力地減小中間層和文件的數量。

因爲鏡像是在構建過程的最後階段構建的,所以能夠經過利用構建緩存來最小化鏡像層。

例如,若是您的構建包含多個鏡像層,則能夠從較不頻繁更改(以確保構建緩存可重用)到更頻繁更改的順序對它們進行排序:

  • 安裝構建應用程序所需的工具  #yum install gcc-c++等

  • 安裝或更新庫依賴項 #yum install nginx

  • 生成您的應用程序  #COPY ./* /var/www/

例如Go應用程序的Dockerfile可能以下所示:

FROM golang:1.9.2-alpine3.6 AS build

# Install tools required for project#安裝項目必須的工具
# Run `docker build --no-cache .` to update dependencies #使用nocache更新依賴
RUN apk add --no-cache git
RUN go get github.com/golang/dep/cmd/dep

# List project dependencies with Gopkg.toml and Gopkg.lock  #列出項目依賴
# These layers are only re-built when Gopkg files are updated #這些層只會在文件有更新時纔會被重構
COPY Gopkg.lock Gopkg.toml /go/src/project/
WORKDIR /go/src/project/
# Install library dependencies
RUN dep ensure -vendor-only

# Copy the entire project and build it  #拷貝整個項目並構建
# This layer is rebuilt when a file changes in the project directory  #當項目目錄裏有文件變化時,這一層將會被從新構建
COPY . /go/src/project/
RUN go build -o /bin/project

# This results in a single layer image  #這樣會致使一個獨立的鏡像層
FROM scratch
COPY --from=build /bin/project /bin/project
ENTRYPOINT ["/bin/project"]
CMD ["--help"]

五、不要安裝沒必要要的包

爲了下降複雜性,依賴性,文件大小和構建時間,避免安裝額外的或沒必要要的軟件包,由於它們可能「很好」。例如,您不須要在數據庫鏡像中包含文本編輯器。

六、解耦應用程序

每一個容器應該只承擔一個功能。

將應用程序分散到多個容器中能夠更容易地水平擴展和重用容器。例如,Web應用程序可能包含三個獨立的容器,每一個容器都有本身獨特的鏡像,以分離的方式管理Web應用程序,數據庫和內存緩存。

將每一個容器限制爲一個進程是一個很好的經驗法則,但它並非一個嚴格的規則。例如,不只能夠使用init進程生成容器 ,並且某些程序可能會自行生成其餘進程。例如,Celery能夠生成多個工做進程,Apache能夠爲每一個請求建立一個進程。

使用您的最佳判斷,儘量保持容器簡單和模塊化。若是容器彼此依賴,則可使用Docker容器網絡來確保這些容器能夠進行通訊。

一個容器就是一個進程,承擔一個功能,這個是最重要的!

七、最小化層數

在舊版本的Docker中,最大限度地減小鏡像中的層數以確保它們具備高性能很是重要。

添加了如下功能以減小此限制:

  • 在Docker 1.10和更高,只有指令RUNCOPYADD建立鏡像。其餘指令建立臨時中間鏡像,而不是直接增長構建的大小。

  • 在Docker 17.05及更高版本中,您能夠執行多階段構建, 並僅將所需的東西複製到最終鏡像中。這容許您在中間構建階段中包含工具和調試信息,而不會增長最終鏡像的大小。

八、對多行參數進行排序

只要有可能,經過按字母數字方式對多行參數進行排序,能夠緩解之後的更改。這有助於避免重複包並使列表更容易更新。這也使PR更容易閱​​讀和審查。在反斜槓(\)以前添加空格也有幫助。

例如:

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

九、利用構建緩存

構建鏡像時,Docker會逐步執行Dockerfile中的指令,按指定的順序執行每一個指令。在檢查每條指令時,Docker會在其緩存中查找能夠重用的現有鏡像,而不是建立新的(重複)鏡像。

若是您根本不想使用緩存,可使用命令中的--no-cache=true選項來docker build。可是,若是你讓Docker使用它的緩存,重要的是要了解它什麼時候能夠找到匹配的鏡像。

Docker遵循的基本規則概述以下:

  • 從已經在高速緩存中的父鏡像開始,將下一條指令與從該基本鏡像導出的全部子鏡像進行比較,以查看它們中的一個是否使用徹底相同的指令構建。若是不是,則緩存無效。

  • 在大多數狀況下,只需將Dockerfile其中一個子鏡像中的指令進行比較就足夠了。可是,某些說明須要更多的檢查和解釋。

  • 對於ADDCOPY指令,檢查鏡像中文件的內容,並計算每一個文件的校驗和。在這些校驗和中不考慮文件的最後修改時間和最後訪問時間。在高速緩存查找期間,將校驗和與現有鏡像中的校驗和進行比較。若是文件中的任何內容(例如內容和元數據)發生了任何變化,則緩存無效。

  • 除了ADDCOPY命令以外,緩存檢查不會查看容器中的文件來肯定緩存匹配。例如,在處理RUN apt-get -y update命令時,不檢查容器中更新的文件以肯定是否存在緩存命中。在這種狀況下,只需使用命令字符串自己來查找匹配項。

一旦高速緩存失效,全部後續Dockerfile命令都會生成新鏡像,而且不使用高速緩存。

 

5、Dockerfile經常使用指令和最佳實踐

  • 只支持Docker本身定義的一套指令,不支持自定義
  • 大小寫不敏感,可是建議所有使用大寫
  • 根據Dockerfile的內容順序執行
Dockerfile經常使用指令以下:
 

一、FROM

 

FROM {base鏡像}
必須放在Dockerfile的第一行,表示從哪一個baseimage開始構建

 

  

 

FROM最佳實踐

儘量使用當前的官方存儲庫做爲鏡像的基礎。
咱們推薦Alpine圖像,由於它受到嚴格控制而且尺寸較小(目前小於5 MB),同時仍然是完整的Linux發行版。

  

二、LABLE最佳實踐

您能夠爲鏡像添加標籤,以幫助按項目組織鏡像,記錄許可信息,幫助實現自動化或出於其餘緣由。

對於每一個標籤,添加LABEL以一個或多個鍵值對開頭的行。如下示例顯示了不一樣的可接受格式。內容包括解釋性意見。

必須引用帶空格的字符串或必須轉義空格。內引號字符(")也必須進行轉義。

鏡像能夠有多個標籤。

# 設置一個或多個標籤
LABEL com.example.version="0.0.1-beta"
LABEL vendor1="ACME Incorporated"
LABEL vendor2=ZENITH\ Incorporated
LABEL com.example.release-date="2015-02-12"
LABEL com.example.version.is-production=""

 

三、MAINTAINER

可選的,用來標識image做者的地方

  

四、RUN

每個RUN指令都會是在一個新的container裏面運行,並提交爲一個image做爲下一個RUN的base
一個Dockerfile中能夠包含多個RUN,按定義順序執行
RUN支持兩種運行方式:
 RUN <cmd> 這個會看成/bin/sh -c 「cmd」 運行
 RUN [「executable」,「arg1」,。。],Docker把他看成json的順序來解析,所以必須使用雙引號,並且executable須要是完整路徑
  RUN 都是啓動一個容器、執行命令、而後提交存儲層文件變動。第一層 RUN command1 的執行僅僅是當前進程,一個內存上的變化而已,其結果不會形成任何文件。而到第二層的時候,啓動的是一個全新的容器,跟第一層的容器更徹底不要緊,天然不可能繼承前一層構建過程當中的內存變化。而若是須要將兩條命令或者多條命令聯合起來執行須要加上&&。如:cd /usr/local/src && wget xxxxxxx
 

RUN最佳實踐

RUN在使用反斜槓分隔的多行上拆分長或複雜語句,以使您Dockerfile更具可讀性,可理解性和可維護性。
 例如:
 RUN apt-get update && apt-get install -y \
        package-bar \
        package-baz \
        package-foo

  使用管道:RUN wget -O - https://some.site | wc -l > /number

 

五、CMD

CMD的做用是做爲執行container時候的默認行爲(容器默認的啓動命令)
當運行container的時候聲明瞭command,則再也不用image中的CMD默認所定義的命令
一個Dockerfile中只能有一個有效的CMD,當定義多個CMD的時候,只有最後一個纔會起做用,即會被覆蓋。
CMD和ENTRPOINT之間的相互關係須要理解,ENTRPOINT不容易被覆蓋,並且docker run中指定的任何參數都會當作參數再次傳遞給ENTRPOINT。
CMD定義的三種方式:
  CMD <cmd> 這個會看成/bin/sh -c "cmd"來執行
  CMD ["executable","arg1",....]
  CMD ["arg1","arg2"],這個時候CMD做爲ENTRYPOINT的參數
 

CMD最佳實踐

在大多數其餘狀況下,CMD應該給出一個交互式shell,例如bash,python和perl。例如,CMD ["perl", "-de0"]CMD ["python"],或CMD [「php」, 「-a」]
使用此表單意味着當您執行相似的操做時 docker run -it python,您將被放入可用的shell中,隨時可使用。 
CMD應該不多的方式使用CMD [「param」, 「param」]會配合ENTRYPOINT,除非你和你預期的用戶已經很是熟悉ENTRYPOINT是如何工做的。

六、EXPOSE聲明端口

格式爲 EXPOSE <端口1> [<端口2>...]。

EXPOSE 指令是聲明運行時容器提供服務端口,這只是一個聲明,在運行時並不會由於這個聲明應用就會開啓這個端口的服務。

在 Dockerfile 中寫入這樣的聲明有兩個好處,

  一、幫助鏡像使用者理解這個鏡像服務的守護端口,以方便配置映射;

  二、在運行時使用隨機端口映射時,也就是 docker run -P 時,會自動隨機映射 EXPOSE 的端口。

EXPOSE最佳實踐:

儘可能使用常規端口,好比Mysql的3306,Mongo的27017。
 

七、ENTRPOINT

entrypoint的做用是,把整個container變成了一個可執行的文件,
這樣不可以經過替換CMD的方法來改變建立container的方式。
可是能夠經過參數傳遞的方法影響到container內部
每一個Dockerfile只可以包含一個entrypoint,多個entrypoint只有最後一個有效
當定義了entrypoint之後,CMD只可以做爲參數進行傳遞

 

entrypoint定義方式:
entrypoint ["executable","arg1","arg2"],這種定義方式下,CMD能夠經過json的方式來定義entrypoint的參數,能夠經過在運行container的時候經過指定command的方式傳遞參數
entrypoint <cmd>,看成/bin/bash -c "cmd"運行命令
 

ENTRPOINT最佳實踐

一、最好的用法是把ENTRYPOINT設置爲鏡像的主命令,容許該鏡像和該命令同樣運行(而後CMD用做默認標誌)。
例如:
ENTRYPOINT ["s3cmd"] CMD ["--help"]
二、ENTRYPOINT指令還能夠與輔助腳本結合使用,使其可以以與上述命令相似的方式運行,即便啓動該工具可能須要多個步驟。
docker-entrpoint.sh文件:
#!/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 "$@"

  

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

八、ADD & COPY

當在源代碼構建的方式下,能夠經過ADD和COPY的方式,把host上的文件或者目錄複製到image中
ADD和COPY的源必須在context路徑下
當src爲網絡URL的狀況下,ADD指令能夠把它下載到dest的指定位置,這個在任何build的方式下均可以work
ADD相對COPY還有一個多的功能,可以進行自動解壓壓縮包。
ADD latest.tar.gz /var/www/wordpress 他會自動將tar包解壓到wordpress目錄下。

ADD & COPY最佳實踐

通常而言,雖然ADD而且COPY在功能上相似,可是COPY 是優選的。由於COPY相對ADD來講,是更透明的,好比ADD在添加tar包時,會自動解壓。
若是您的Dockerfile有多個步驟使用上下文中的不一樣文件,則COPY它們是單獨的,而不是一次性完成。這可確保每一個步驟的構建緩存僅在特定所需文件更改時失效(強制從新執行該步驟)。
COPY requirements.txt /tmp/
RUN pip install --requirement /tmp/requirements.txt
COPY . /tmp/

  儘可能減少鏡像文件大小,不是一次性的拷貝文件,而是隻拷貝須要的文件,這樣鏡像文件會更小。

因爲鏡像大小很重要,ADD所以強烈建議不要使用從遠程URL獲取包。你應該使用curlwget代替。這樣,您能夠刪除提取後再也不須要的文件,也沒必要在圖像中添加其餘圖層。例如,你應該避免如下作法:
ADD http://example.com/big.tar.xz /usr/src/things/
RUN tar -xJf /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

九、ENV

ENV key value
用來設置環境變量,後續的RUN可使用它所建立的環境變量
當建立基於該鏡像的container的時候,會自動擁有設置的環境變量

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

每一行ENV就會建立一個新的中間層,就像RUN命令同樣。這意味着即便您在未來的鏡像中取消設置環境變量,它仍然會在此圖層中保留,而且能夠轉儲其值。

您能夠經過建立以下所示的Dockerfile來測試它,而後構建它。

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

mark

要防止這種狀況發生,而且確實取消了以前設置的環境變量,請使用RUN帶有shell命令的命令,在單個鏡像中設置,使用和取消設置變量all。您可使用;或分隔命令&&

若是您使用第二種方法,而且若是其中一個命令失敗,則docker build也會失敗。這一般是一個好主意。使用\做爲行繼續符能夠提升可讀性。

您還能夠將全部命令放入shell腳本中,並讓RUN命令運行該shell腳本。 

十、WORKDIR

用來指定當前工做目錄(或者稱爲當前目錄)
當使用相對目錄的狀況下,採用上一個WORKDIR指定的目錄做爲基準 

WORKDIR最佳實踐

爲了清晰和可靠,您應該始終使用絕對路徑的 WORKDIR。此外,您應該使用WORKDIR,而不是使用難以閱讀,排除故障和維護指令RUN cd … && do-something

十一、USER

 

指定UID或者username,來決定運行RUN指令的用戶

 

  

 

USER最佳實踐
若是服務能夠在沒有權限的狀況下運行,請把USER更改成非root用戶。
首先在Dockerfile中建立用戶和組RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres。
鏡像中的用戶和組被分配了不肯定的UID/GID,由於不管鏡像如何構建,都會分配一個UID/GID。所以,若是它很重要,您應該分配一個顯式的UID / GID。
因爲Go歸檔/tar包在處理鬆散文件時有一個未解決的錯誤,嘗試在Docker容器內建立具備很是大的UID的用戶可能致使磁盤耗盡,由於/var/log/faillog在容器層中填充了NULL(\ 0)字符。
解決方法是將--no-log-init標誌傳遞給useradd。Debian / Ubuntu adduser裝飾器不支持此標誌。

避免安裝或使用sudo,由於它具備可能致使不可預測的TTY和信號轉發行爲的問題。若是您絕對須要相似的功能sudo,例如將守護程序初始化root爲非運行它root,請考慮使用「gosu」

最後,爲了減小層次和複雜性,避免USER頻繁地來回切換。

十二、ONBUILD

ONBUILD做爲一個trigger的標記,能夠用來trigger任何Dockerfile中的指令
能夠定義多個ONBUILD指令
當下一個鏡像B使用鏡像A做爲base的時候,在FROM A指令前,會先按照順序執行在構建A時候定義的ONBUILD指令
ONBUILD <DOCKERFILE 指令> <content>

ONBUILD最佳實踐

ONBUILD指令能爲鏡像添加觸發器,當一個鏡像被用做其餘鏡像的基礎鏡像時,改鏡像中的觸發器將會被執行。

ONBUILD指令是緊跟在FROM以後指定的

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

把時要當心,ADDCOPYONBUILD。若是新構建的上下文缺乏正在添加的資源,則「onbuild」映像將發生災難性故障。

例如:

FROM ubuntu:14.04
MAINTAINER xuequn "xuequn@kingsoft.com"
RUN apt-get update
RUN apt-get install -y apache2
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
ONBUILD ADD . /var/www/
EXPOSE 80
ENTRPOINT ["/usr/sbin/apache2"]
CMD ["-D","FOREGOUND"]

 

  在新構建的鏡像包含一條ONBUILD指令,該指令會使用ADD 指令將構建環境所在的目錄下的全部文件拷貝到/var/www/下面。

當咱們使用上面的鏡像做爲基礎鏡像,再構建一個新的鏡像時:

FROM xuequn/apache2
MAINTAINER xuequn 'xuequn@kingsoft.com'
ENV APPLICATION_NAME webapp01

  當執行完FROM時,就進入了構建階段,此時會出發基礎鏡像中的ONBUILD指令,會將當期目錄下的全部文件拷貝到/var/www/下面,這樣就完成了個性化鏡像製做功能,這就是ONBUILD的絕妙之處!!

注意:

  • ONBUILD指令只會被繼承一次,也就是在子鏡像製做時會出發ONBUILD指令,而孫子鏡像構建時不會再觸發此指令!
  • ONBUILD指令中有幾條指令是不能使用的:FROM/MAINTAINER/ONBUILD。由於這樣會進入遞歸調用而進入死循環!

1三、VOLUME

用來建立一個在image以外的mount point,用來在多個container之間實現數據共享
運行使用json array的方式定義多個volume
VOLUME ["/var/data1","/var/data2"]
或者plain text的狀況下定義多個VOLUME指令

VOLUME最佳實踐

該VOLUME指令應用於公開由docker容器建立的任何數據庫存儲區域,配置存儲或文件/文件夾。
相關文章
相關標籤/搜索