Dockerfile命令詳解及最佳實踐

1. Dockerfile 命令詳解:

  • FROM 指定基礎(chǔ)鏡像(必選)

    所謂定制鏡像冀值,那一定是以一個鏡像為基礎(chǔ)箕般,在其上進行定制纯赎。就像我們之前運行了一個 nginx 鏡像的容器巡莹,再進行修改一樣泰讽,基礎(chǔ)鏡像是必須指定的。而FROM就是指定基礎(chǔ)鏡像薪贫,因此一個 Dockerfile 中 FROM 是必備的指令恍箭,并且必須是第一條指令

    Docker hub上有非常多的高質(zhì)量的官方鏡像瞧省,有可以直接拿來使用的服務(wù)類的鏡像扯夭,如 nginxredis鞍匾、mongo交洗、mysqlhttpd橡淑、php构拳、tomcat 等;也有一些方便開發(fā)梁棠、構(gòu)建隐圾、運行各種語言應(yīng)用的鏡像,如 node掰茶、openjdk暇藏、pythonruby濒蒋、golang 等盐碱。

    如果沒有找到對應(yīng)服務(wù)的鏡像,官方鏡像中還提供了一些更為基礎(chǔ)的操作系統(tǒng)鏡像沪伙,如 ubuntu瓮顽、debiancentos围橡、fedora暖混、alpine 等。

    FROM命令語法:

    FROM <image>:<tag>
    

    如果tag沒有選擇翁授,默認(rèn)為latest拣播。

    除了選擇現(xiàn)有鏡像為基礎(chǔ)鏡像外,Docker 還存在一個特殊的鏡像收擦,名為scratch贮配。這個鏡像是虛擬的概念,并不實際存在塞赂,它表示一個空白的鏡像泪勒。

    FROM scratch
    ...
    

    如果你以scratch為基礎(chǔ)鏡像的話,意味著你不以任何鏡像為基礎(chǔ),接下來所寫的指令將作為鏡像第一層開始存在圆存。有的同學(xué)可能感覺很奇怪叼旋,沒有任何基礎(chǔ)鏡像,我怎么去執(zhí)行我的程序呢沦辙,其實對于 Linux 下靜態(tài)編譯的程序來說夫植,并不需要有操作系統(tǒng)提供運行時支持,所需的一切庫都已經(jīng)在可執(zhí)行文件里了怕轿,因此直接FROM scratch會讓鏡像體積更加小巧偷崩。使用 Go 語言 開發(fā)的應(yīng)用很多會使用這種方式來制作鏡像,這也是為什么有人認(rèn)為 Go 是特別適合容器微服務(wù)架構(gòu)的語言的原因之一撞羽。

    下面我們以一個go語言的helloworld為例:

    FROM scratch
    
    COPY helloworld /
    COPY hellowold2 /
    CMD ["./helloworld"]
    

    helloworld文件就是個go語言編譯出來的可執(zhí)行程序阐斜,只會打印出hello world

    docker build -t hello-go:v1 .
    
    docker run hello-go:v1
    
  • LABEL 設(shè)置鏡像元數(shù)據(jù)

    使用LABEL指令诀紊,可以為鏡像設(shè)置元數(shù)據(jù)谒出,例如鏡像創(chuàng)建者或者鏡像說明。舊版的Dockerfile語法使用MAINTAINER指令指定鏡像創(chuàng)建者邻奠,但是它已經(jīng)被棄用了笤喳。

    LABEL命令語法:

    LABEL <key>=<value> <key>=<value> <key>=<value> ...
    

    一個Dockerfile種可以有多個LABEL,如下:

    LABEL maintainer="cerberus43@gmail.com"
    LABEL version="1.0"
    LABEL description="This is a test dockerfile"
    

    但是并不建議這樣寫碌宴,最好就寫成一行杀狡,如太長需要換行的話則使用\符號。

    如下:

    LABEL maintainer="cerberus43@gmail.com" \
    version="1.0" \
    description="This is a test dockerfile"
    

    說明:LABEL會繼承基礎(chǔ)鏡像種的LABEL贰镣,如遇到key相同呜象,則值覆蓋。

  • RUN 運行命令

    使用RUN指令碑隆,可以用來執(zhí)行命令行的命令恭陡。

    RUN命令有兩種語法:

    • shell格式:

      在linux操作系統(tǒng)上默認(rèn) /bin/sh -c

      RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html
      
    • exec格式:

      RUN ["可執(zhí)行文件", "參數(shù)1", "參數(shù)2"]
      

    注意:多行命令不要寫多個RUN,原因是Dockerfile中每一個指令都會建立一層上煤,多少個RUN就構(gòu)建了多少層鏡像休玩,會造成鏡像的臃腫、多層劫狠,不僅僅增加了構(gòu)件部署的時間拴疤,還容易出錯。

    下面是一個使用apt-get安裝多個包的例子:

    RUN apt-get update && apt-get install -y \  
     bzr \
     cvs \
     git \
     mercurial \
     subversion
    
  • COPY 復(fù)制文件

    COPY命令有兩種語法格式:

    • COPY [--chown=<user>:<group>] <源路徑>... <目標(biāo)路徑>
      
    • COPY [--chown=<user>:<group>] ["<源路徑1>",... "<目標(biāo)路徑>"]
      

    RUN 指令一樣嘉熊,也有兩種格式遥赚,一種類似于命令行,一種類似于函數(shù)調(diào)用阐肤。

    說明:

    • 目標(biāo)路徑可以是容器內(nèi)的絕對路徑,也可以是相對于工作目錄的相對路徑(工作目錄可以用WORKDIR指令來指定)。
    • 目標(biāo)路徑不需要事先創(chuàng)建孕惜,如果目錄不存在會在復(fù)制文件前先行創(chuàng)建缺失目錄愧薛。
    • 使用 COPY 指令,源文件的各種元數(shù)據(jù)都會保留衫画。比如讀毫炉、寫、執(zhí)行權(quán)限削罩、文件變更時間等瞄勾。

    復(fù)制單個文件示例:

    COPY package.json /usr/src/app/
    

    <源路徑>可以是多個,甚至可以是通配符弥激,其通配符規(guī)則要滿足 Go 的 filepath.Match 規(guī)則进陡,如:

    COPY hom* /mydir/
    COPY hom?.txt /mydir/
    

    復(fù)制src目錄下內(nèi)容到 /tmp 目錄下:

    COPY src/ /tmp
    

    復(fù)制多個目錄下內(nèi)容到 /tmp 目錄下:

    COPY src1/ src2/ /tmp
    

    上面的命令只會將文件夾內(nèi)容復(fù)制到鏡像目錄下,復(fù)制整個src目錄到/tmp目錄下微服,如果源目錄名不存在將自動逐級創(chuàng)建:

    COPY src/ /tmp/src
    

    指定文件權(quán)限

    在使用該指令的時候還可以加上 --chown=: 選項來改變文件的所屬用戶及所屬組趾疚。

    COPY --chown=devuser:devgroup files* /mydir/
    
  • ADD 更高級的復(fù)制文件

    ADD 命令和 COPY 的格式和性質(zhì)基本一致。但是在 COPY 基礎(chǔ)上增加了一些功能以蕴。

    • 解壓壓縮文件并把它們添加到鏡像中:

      WORKDIR /app
      ADD nginx.tar.gz .
      
    • 從 url 拷貝文件到鏡像中:

      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
      

      但是在Dockerfile 最佳實踐官方文檔中卻強烈建議不要這么用糙麦!官方建議我們當(dāng)需要從遠程復(fù)制文件時,最好使用curl或wget命令來代替ADD命令丛肮。原因是赡磅,當(dāng)使用ADD命令時,會創(chuàng)建更多的鏡像層宝与,當(dāng)然鏡像也會變的更大焚廊。

      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
      

    在 Docker 官方的 Dockerfile 最佳實踐官方文檔 中要求,盡可能的使用 COPY伴鳖,因為 COPY 的語義很明確节值,就是復(fù)制文件而已,而 ADD 則包含了更復(fù)雜的功能榜聂,其行為也不一定很清晰搞疗。最適合使用 ADD 的場合,就是所提及的需要自動解壓縮的場合须肆。

    因此在 COPY和 ADD指令中選擇的時候匿乃,可以遵循這樣的原則,所有的文件復(fù)制均使用COPY指令豌汇,僅在需要自動解壓縮的場合使用ADD幢炸。

  • WORKDIR 指定工作目錄

    使用 WORKDIR 指令可以來指定工作目錄(或者稱為當(dāng)前目錄),以后各層的當(dāng)前目錄就被改為指定的目錄拒贱,如該目錄不存在宛徊,WORKDIR 會幫你建立目錄佛嬉。

    語法格式為:

    WORKDIR <工作目錄路徑>
    
    FROM centos:7.2
    
    #創(chuàng)建/usr/local/tomcat目錄
    RUN mkdir /usr/local/tomcat
    
    #定位到tomcat下載目錄
    WORKDIR /usr/local/tomcat
    
    #wget tomcat到/usr/local/tomcat目錄
    RUN wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-7/v7.0.86/bin/apache-tomcat-7.0.86.tar.gz
    
  • ENV 指定容器的環(huán)境變量

    使用ENV指令,可以設(shè)置環(huán)境變量闸天,無論是后面的其它指令暖呕,如 RUN,還是運行時的應(yīng)用苞氮,都可以直接使用這里定義的環(huán)境變量湾揽。

    語法格式有兩種:

    • ENV <key> <value>
      
    • ENV <key1>=<value1> <key2>=<value2>...
      

    定義了環(huán)境變量,那么在后續(xù)的指令中笼吟,就可以使用這個環(huán)境變量库物。比如在官方 node 鏡像 Dockerfile 中,就有類似這樣的代碼:

    ENV NODE_VERSION 7.2.0
    
    RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
      && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
      && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
      && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
      && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \
      && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
      && ln -s /usr/local/bin/node /usr/local/bin/nodejs
    

    在這里先定義了環(huán)境變量 NODE_VERSION贷帮,其后的 RUN 這層里戚揭,多次使用 $NODE_VERSION 來進行操作定制∶笊#可以看到毫目,將來升級鏡像構(gòu)建版本的時候,只需要更新 7.2.0 即可诲侮,Dockerfile 構(gòu)建維護變得更輕松了镀虐。

  • ARG 指定Dockerfile中的環(huán)境變量

    ARGARG定義的變量用于構(gòu)建Docker鏡像,在把Dockerfile構(gòu)建成鏡像后沟绪,ARG定義的變量便不在起作用刮便;

    ENVENV定義的變量用于容器的環(huán)境變量,在Dockerfile里定義后绽慈,在容器的運行時是可以使用這個變量的恨旱;

    上面可能讀起來比較繞,看下這個實例就明白了:

    ARG VAR_A=1
    ENV VAR_B ${VAR_A}
    

    通過構(gòu)建鏡像并啟動容器后坝疼,查看環(huán)境變量如下:

    $ docker exec ContainerID env
    VAR_B=1
    

    從實例可看出搜贤,ARG定義的變量在Dockerfile中使用,構(gòu)建完鏡像后钝凶,就下崗仪芒;而ENV定義的變量會帶入容器的環(huán)境變量。

    image

    通掣荩可以把ARG與ENV結(jié)合使用:

    ARG buildtime_variable=default_value
    ENV env_var_name=$buildtime_variable 
    

    使用這種方式可以解決Dockerfile硬編碼的問題掂名,比如在微服務(wù)下很多服務(wù)的情況下,構(gòu)建一個鏡像修改一次Dockerfile哟沫,而使用這種方式Dockerfile是不變的饺蔑,只需要在docker build的時候加上參數(shù)值就可以。

  • CMD 指定鏡像啟動時的命令

    首先我們看官網(wǎng)對CMD的定義:

    The main purpose of a CMD is to provide defaults for an executing container. These defaults can include an executable, or they can omit the executable, in which case you must specify an ENTRYPOINT instruction as well.
    

    意思是嗜诀,CMD給出的是一個容器的默認(rèn)的可執(zhí)行體猾警。也就是容器啟動以后孔祸,默認(rèn)的執(zhí)行的命令。重點就是這個默認(rèn)肿嘲。意味著融击,如果docker run沒有指定任何的執(zhí)行命令或者Dockerfile里面也沒有ENTRYPOINT筑公,那么雳窟,就會使用CMD指定的默認(rèn)的執(zhí)行命令執(zhí)行。同時也從側(cè)面說明了ENTRYPOINT的含義匣屡,它才是真正的容器啟動以后要執(zhí)行命令封救。

    所以這句話就給出了CMD命令的一個角色定位,它主要作用是默認(rèn)的容器啟動執(zhí)行命令捣作。(注意不是“全部”作用)

    這也是為什么大多數(shù)網(wǎng)上博客論壇說的“CMD會被覆蓋”誉结,其實為什么會覆蓋?因為CMD的角色定位就是默認(rèn)券躁,如果你不額外指定惩坑,那么就執(zhí)行CMD的命令,否則呢也拜?只要你指定了以舒,那么就不會執(zhí)行CMD,也就是CMD會被覆蓋慢哈。

    比如蔓钟,ubuntu 鏡像默認(rèn)的 CMD/bin/bash,如果我們直接 docker run -it ubuntu 的話卵贱,會直接進入 bash滥沫。我們也可以在運行時指定運行別的命令,如 docker run -it ubuntu cat /etc/os-release键俱。這就是用 cat /etc/os-release 命令替換了默認(rèn)的 /bin/bash 命令了兰绣,輸出了系統(tǒng)版本信息。

    明白了CMD命令的主要用途编振。下面就看看具體用法:

    The CMD instruction has three forms:
     
    CMD ["executable","param1","param2"] (exec form, this is the preferred form)  #exec格式缀辩,首選方法
    CMD ["param1","param2"] (as default parameters to ENTRYPOINT) #為ENTRYPOINT傳參用法
    CMD command param1 param2 (shell form)    #shell格式
    

    因為還沒有講ENTRYPOINT,所以先不用看第二種用法党觅。

    在指令格式上雌澄,一般推薦使用 exec 格式,這類格式在解析時會被解析為 JSON 數(shù)組杯瞻,因此一定要使用雙引號 "镐牺,而不要使用單引號。

    如果使用 shell 格式的話魁莉,實際的命令會被包裝為 sh -c 的參數(shù)的形式進行執(zhí)行睬涧。比如:

    CMD echo $HOME
    

    在實際執(zhí)行中募胃,會將其變更為:

    CMD [ "sh", "-c", "echo $HOME" ]
    

    這就是為什么我們可以使用環(huán)境變量的原因,因為這些環(huán)境變量會被 shell 進行解析處理畦浓。

    提到 CMD 就不得不提容器中應(yīng)用在前臺執(zhí)行和后臺執(zhí)行的問題痹束。這是常出現(xiàn)的一個混淆。

    Docker 不是虛擬機讶请,容器中的應(yīng)用都應(yīng)該以前臺執(zhí)行祷嘶,而不是像虛擬機、物理機里面那樣夺溢,用 systemd 去啟動后臺服務(wù)论巍,容器內(nèi)沒有后臺服務(wù)的概念。

    如有人會把寫成這樣:

    CMD service nginx start
    

    然后發(fā)現(xiàn)容器執(zhí)行后就立即退出了风响。這就是因為沒有搞明白前臺嘉汰、后臺的概念,沒有區(qū)分容器和 虛擬機的差異状勤,依舊在以傳統(tǒng)虛擬機的角度去理解容器鞋怀。

    對于容器而言,其啟動程序就是容器應(yīng)用進程持搜,容器就是為了主進程而存在的密似,主進程退出,容器就失去了存在的意義朵诫,從而退出辛友,其它輔助進程不是它需要關(guān)心的東西。

    而使用 service nginx start 命令剪返,則是希望以后臺守護進程形式啟動 nginx 服務(wù)废累。而剛才說了 CMD service nginx start 會被理解為 CMD [ "sh", "-c", "service nginx start"],因此主進程實際上是 sh脱盲。那么當(dāng) service nginx start 命令結(jié)束后邑滨,sh 也就結(jié)束了,sh 作為主進程退出了钱反,自然就會令容器退出掖看。

    正確的做法是直接執(zhí)行 nginx 可執(zhí)行文件,并且要求以前臺形式運行:

    CMD ["nginx", "-g", "daemon off;"]
    
  • ENTRYPOINT 指定容器入口命令

    首先我們看官網(wǎng)對ENTRYPOINT的定義:

    An ENTRYPOINT allows you to configure a container that will run as an executable.
    

    也就是說ENTRYPOINT才是正統(tǒng)地用于定義容器啟動以后的執(zhí)行體的面哥,其實我們從名字也可以理解哎壳,這個是容器的“入口”。

    它有兩種用法:

    ENTRYPOINT has two forms:
     
    ENTRYPOINT ["executable", "param1", "param2"] (exec form, preferred)  #exec格式尚卫,首選方法
    ENTRYPOINT command param1 param2 (shell form) #shell格式
    

    先看exec命令行模式归榕,也就是帶中括號的。如果docker run命令后面有東西,那么后面的全部都會作為ENTRYPOINT的參數(shù)旁赊。如果docker run后面沒有額外的東西趟大,但是CMD有喷众,那么CMD的全部內(nèi)容會作為ENTRYPOINT的參數(shù),這同時是CMD的第二種用法挖函。這也是網(wǎng)上說的ENTRYPOINT不會被覆蓋巷怜。當(dāng)然如果要在docker run里面覆蓋呢袱,也是有辦法的姆蘸,使用--entrypoint即可墩莫。

    可能光看文字有點迷糊,下面看個例子:

    FROM alpine
    
    ENTRYPOINT ["echo"]
    
    CMD ["CMD"]
    
    docker build -t entrypoint-test:v1 .
    
    #會打印出CMD中定義的輸出“CMD”
    docker run --rm entrypoint-test:v1
    $CMD
    
    #會打印出docker run中傳入的“docker run”覆蓋CMD中的定義
    docker run --rm entrypoint-test:v1 docker run
    $docker run
    

    第二種是shell模式的乞旦。在這種模式下贼穆,任何docker runCMD的參數(shù)都無法被傳入到ENTRYPOINT里。所以官網(wǎng)推薦第一種用法兰粉。

    FROM alpine
    
    ENTRYPOINT echo
    
    CMD ["CMD"]
    
    docker build -t entrypoint-test:v2 .
    
    #不會打印出CMD中定義的“CMD”
    docker run --rm entrypoint-test:v2
    $
    
    #不會打印出docker run中傳入的“docker run”
    docker run --rm entrypoint-test:v2 docker run
    $
    

    最后總結(jié)下一般該怎么使用:

    一般還是會用ENTRYPOINT的中括號形式作為docker 容器啟動以后的默認(rèn)執(zhí)行命令,里面放的是不變的部分顶瞳,可變部分比如命令參數(shù)可以使用CMD的形式提供默認(rèn)版本玖姑,也就是執(zhí)行docker run里面沒有任何參數(shù)時使用的默認(rèn)參數(shù)。如果我們想用默認(rèn)參數(shù)慨菱,就直接docker run焰络,如果想用其他參數(shù),就在docker run后面加想要的參數(shù)符喝。

    ENTRYPOINT ["python3", "manage.py", "runserver"]
    
    CMD ["0.0.0.0:8000"]
    
  • EXPOSE 暴露端口

    格式為 EXPOSE <端口1> [<端口2>...]闪彼。

    EXPOSE 指令是聲明運行時容器提供服務(wù)端口,這只是一個聲明协饲,在運行時并不會因為這個聲明應(yīng)用就會開啟這個端口的服務(wù)畏腕。在 Dockerfile 中寫入這樣的聲明有兩個好處,一個是幫助鏡像使用者理解這個鏡像服務(wù)的守護端口茉稠,以方便配置映射描馅;另一個用處則是在運行時使用隨機端口映射時,也就是 docker run -P 時而线,會自動隨機映射 EXPOSE 的端口铭污。

    要將 EXPOSE 和在運行時使用 -p <宿主端口>:<容器端口> 區(qū)分開來。-p膀篮,是映射宿主端口和容器端口嘹狞,換句話說,就是將容器的對應(yīng)端口服務(wù)公開給外界訪問誓竿,而 EXPOSE 僅僅是聲明容器打算使用什么端口而已磅网,并不會自動在宿主進行端口映射。

  • VOLUME 定義匿名卷

    VOLUME指令用于暴露任何數(shù)據(jù)庫存儲文件烤黍,配置文件知市,或容器創(chuàng)建的文件和目錄傻盟。強烈建議使用 VOLUME來管理鏡像中的可變部分和用戶可以改變的部分。

    兩種使用方法的格式為:

    VOLUME ["<路徑1>", "<路徑2>"...]
    VOLUME <路徑>
    

    之前我們說過嫂丙,容器運行時應(yīng)該盡量保持容器存儲層不發(fā)生寫操作娘赴,對于數(shù)據(jù)庫類需要保存動態(tài)數(shù)據(jù)的應(yīng)用,其數(shù)據(jù)庫文件應(yīng)該保存于卷中跟啤。為了防止運行時用戶忘記將動態(tài)文件所保存目錄掛載為卷诽表,在 Dockerfile 中,我們可以事先指定某些目錄掛載為匿名卷隅肥,這樣在運行時如果用戶不指定掛載竿奏,其應(yīng)用也可以正常運行,不會向容器存儲層寫入大量數(shù)據(jù)腥放。

    VOLUME /data
    

    這里的 /data 目錄就會在運行時自動掛載為匿名卷泛啸,任何向 /data 中寫入的信息都不會記錄進容器存儲層,從而保證了容器存儲層的無狀態(tài)化秃症。

  • ONBUILD

    ONBUILD指令可以為鏡像添加觸發(fā)器候址。其參數(shù)是任意一個Dockerfile指令。

    當(dāng)我們在一個Dockerfile文件中加上ONBUILD指令种柑,該指令對利用該Dockerfile構(gòu)建鏡像(A鏡像)不會產(chǎn)生實質(zhì)性影響岗仑。

    但是當(dāng)我們編寫一個新的Dockerfile文件來基于A鏡像構(gòu)建一個鏡像(比如為B鏡像)時,這時構(gòu)造A鏡像的Dockerfile文件中的ONBUILD指令就生效了聚请,在構(gòu)建B鏡像的過程中荠雕,首先會執(zhí)行ONBUILD指令指定的指令,然后才會執(zhí)行其它指令驶赏。

    需要注意的是炸卑,如果是再利用B鏡像構(gòu)造新的鏡像時,那個ONBUILD指令就無效了母市,也就是說只能再構(gòu)建子鏡像中執(zhí)行矾兜,對孫子鏡像構(gòu)建無效。其實想想是合理的患久,因為在構(gòu)建子鏡像中已經(jīng)執(zhí)行了椅寺,如果孫子鏡像構(gòu)建還要執(zhí)行,相當(dāng)于重復(fù)執(zhí)行蒋失,這就有問題了返帕。

    利用ONBUILD指令,實際上就是相當(dāng)于創(chuàng)建一個模板鏡像,后續(xù)可以根據(jù)該模板鏡像創(chuàng)建特定的子鏡像篙挽,需要在子鏡像構(gòu)建過程中執(zhí)行的一些通用操作就可以在模板鏡像對應(yīng)的Dockerfile文件中用ONBUILD指令指定荆萤。 從而減少Dockerfile文件的重復(fù)內(nèi)容編寫。

    例如:

    先編寫個onbuild-test:a鏡像:

    FROM alpine
    
    LABEL maintainer="cerberus43@gmail.com"
    
    ONBUILD RUN echo "onbuild" >> test.txt
    
    CMD ["cat", "test.txt"]
    
    $docker build -t onbuild-test:a .
    
    $docker run --rm onbuild-test:a
    

    再編寫個onbuild-test:b鏡像:

    FROM onbuild-test:a
    
    $docker build -t onbuild-test:b .
    
    $docker run --rm onbuild-test:b
    

2. Dockerfile最佳實踐:

官方原文:Dockerfile最佳實踐

  • 容器應(yīng)該是短暫的

    通過 Dockerfile 構(gòu)建的鏡像所啟動的容器應(yīng)該盡可能短暫(生命周期短)×淳拢「短暫」意味著可以停止和銷毀容器偏竟,并且創(chuàng)建一個新容器并部署好所需的設(shè)置和配置工作量應(yīng)該是極小的。我們可以查看下12 Factor(12要素)應(yīng)用程序方法的進程部分敞峭,可以讓我們理解這種無狀態(tài)方式運行容器的動機踊谋。

  • 理解上下文context

    如果注意,會看到 docker build 命令最后有一個.旋讹。.表示當(dāng)前目錄殖蚕,而 Dockerfile 就在當(dāng)前目錄,因此不少人以為這個路徑是在指定Dockerfile 所在路徑沉迹,這么理解其實是不準(zhǔn)確的睦疫。如果對應(yīng)上面的命令格式,你可能會發(fā)現(xiàn)鞭呕,這是在指定上下文路徑context蛤育。那么什么是上下文呢?

    首先我們要理解 docker build 的工作原理琅拌。Docker 在運行時分為 Docker引擎(也就是服務(wù)端守護進程)和客戶端工具缨伊。Docker 的引擎提供了一組 REST API,被稱為 Docker Remote API进宝,而如 docker 命令這樣的客戶端工具,則是通過這組 APIDocker 引擎交互枷恕,從而完成各種功能党晋。因此,雖然表面上我們好像是在本機執(zhí)行各種 docker 功能徐块,但實際上未玻,一切都是使用的遠程調(diào)用形式在服務(wù)端(Docker 引擎)完成。也因為這種 C/S 設(shè)計胡控,讓我們操作遠程服務(wù)器的 Docker 引擎變得輕而易舉扳剿。

    當(dāng)我們進行鏡像構(gòu)建的時候,并非所有定制都會通過 RUN 指令完成昼激,經(jīng)常會需要將一些本地文件復(fù)制進鏡像庇绽,比如通過 COPY 指令、ADD 指令等橙困。而 docker build 命令構(gòu)建鏡像瞧掺,其實并非在本地構(gòu)建,而是在服務(wù)端凡傅,也就是 Docker 引擎中構(gòu)建的辟狈。那么在這種客戶端/服務(wù)端的架構(gòu)中,如何才能讓服務(wù)端獲得本地文件呢?

    這就引入了上下文的概念哼转。當(dāng)構(gòu)建的時候明未,用戶會指定構(gòu)建鏡像上下文的路徑,docker build 命令得知這個路徑后壹蔓,會將路徑下的所有內(nèi)容打包趟妥,然后上傳給Docker 引擎。這樣 Docker 引擎收到這個上下文包后庶溶,展開就會獲得構(gòu)建鏡像所需的一切文件煮纵。如果在 Dockerfile 中這么寫:

    COPY ./package.json /app/
    

    這并不是要復(fù)制執(zhí)行 docker build 命令所在的目錄下的package.json,也不是復(fù)制 Dockerfile 所在目錄下的 package.json偏螺,而是復(fù)制 上下文(context) 目錄下的 package.json行疏。

    因此,COPY這類指令中的源文件的路徑都是相對路徑套像。這也是初學(xué)者經(jīng)常會問的為什么 COPY ../package.json /app 或者 COPY /opt/xxxx /app 無法工作的原因酿联,因為這些路徑已經(jīng)超出了上下文的范圍,Docker 引擎無法獲得這些位置的文件夺巩。如果真的需要那些文件贞让,應(yīng)該將它們復(fù)制到上下文目錄中去。

    現(xiàn)在就可以理解剛才的命令docker build -t nginx:v3 .中的這個.柳譬,實際上是在指定上下文的目錄喳张,docker build 命令會將該目錄下的內(nèi)容打包交給Docker 引擎以幫助構(gòu)建鏡像。

    如果觀察 docker build 輸出美澳,我們其實已經(jīng)看到了這個發(fā)送上下文的過程:

    $ docker build -t nginx:v3 .
    Sending build context to Docker daemon 2.048 kB
    ...
    

    理解構(gòu)建上下文對于鏡像構(gòu)建是很重要的销部。context過大會造成docker build很耗時,鏡像過大則會造成docker pull/push性能變差以及運行時容器體積過大浪費空間資源制跟。

    一般來說舅桩,應(yīng)該會將 Dockerfile 置于一個空目錄下,或者項目根目錄下雨膨。如果該目錄下沒有所需文件擂涛,那么應(yīng)該把所需文件復(fù)制一份過來。如果目錄下有些東西確實不希望構(gòu)建時傳給 Docker 引擎聊记,那么可以用 .gitignore 一樣的語法寫一個.dockerignore撒妈,該文件是用于剔除不需要作為上下文傳遞給 Docker 引擎的。

    那么為什么會有人誤以為. 是指定 Dockerfile 所在目錄呢甥雕?這是因為在默認(rèn)情況下踩身,如果不額外指定 Dockerfile 的話,會將上下文目錄下的名為Dockerfile 的文件作為 Dockerfile社露。

    這只是默認(rèn)行為挟阻,實際上 Dockerfile 的文件名并不要求必須為 Dockerfile,而且并不要求必須位于上下文目錄中,比如可以用-f ../Dockerfile.php參數(shù)指定某個文件作為 Dockerfile附鸽。

  • 使用.dockerignore文件

    使用 Dockerfile 構(gòu)建鏡像時最好是將 Dockerfile 放置在一個新建的空目錄下脱拼。然后將構(gòu)建鏡像所需要的文件添加到該目錄中。為了提高構(gòu)建鏡像的效率坷备,你可以在目錄下新建一個.dockerignore文件來指定要忽略的文件和目錄熄浓。.dockerignore 文件的排除模式語法和Git.gitignore 文件相似。

  • 使用多段構(gòu)建

    多階段構(gòu)建從Docker 17.05及更高版本的守護進程與客戶端的新功能省撑, 對于那些努力優(yōu)化Dockerfile同時保持可閱讀性和可維護性的人來說赌蔑,多階段構(gòu)建是非常有用的。

    一個Dockerfile用于開發(fā)環(huán)境竟秫,其中包含構(gòu)建應(yīng)用程序所需的一切娃惯, 另一個精簡版的Dockerfile,只包含你的應(yīng)用程序及運行所需的內(nèi)容肥败,用于生產(chǎn)環(huán)境趾浅, 這種情況實際上非常普遍,這被稱為”構(gòu)建器模式”馒稍。維護兩個Dockerfile并不理想皿哨。

    下面是一個Dockerfile.buildDockerfile的示例,采用上面的構(gòu)建器模式:

    Dockerfile.build

    FROM golang:1.7.3
    WORKDIR /go/src/github.com/alexellis/href-counter/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go .
    RUN go get -d -v golang.org/x/net/html \
      && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
    

    Dockerfile

    FROM alpine:latest
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY app .
    CMD ["./app"]
    

    build.sh

    #!/bin/sh
    echo Building alexellis2/href-counter:build
    
    docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \
        -t alexellis2/href-counter:build . -f Dockerfile.build
    
    docker create --name extract alexellis2/href-counter:build
    docker cp extract:/go/src/github.com/alexellis/href-counter/app ./app
    docker rm -f extract
    
    echo Building alexellis2/href-counter:latest
    
    docker build --no-cache -t alexellis2/href-counter:latest .
    rm ./app
    

    運行build.sh時纽谒,你需要先構(gòu)建第一個鏡像证膨,創(chuàng)建一個容器以便將結(jié)果復(fù)制出來,然后構(gòu)建第二個鏡像鼓黔。 兩個鏡像都會占用你的系統(tǒng)空間椎例,并且在你的本地磁盤上依然有應(yīng)用程序。

    在多階段構(gòu)建下请祖,你可以在Dockerfile中使用多個FROM聲明,每個FROM聲明可以使用不同的基礎(chǔ)鏡像脖祈, 并且每個FROM都使用一個新的構(gòu)建階段肆捕。你可以選擇性的將文件從一個階段復(fù)制到另一個階段, 刪除你不想保留在最終鏡像中的一切盖高。我們來調(diào)整上面的Dockerfile以使用多階段構(gòu)建做個示例。

    FROM golang:1.7.3
    WORKDIR /go/src/github.com/alexellis/href-counter/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go .
    RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
    
    FROM alpine:latest
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
    CMD ["./app"]
    

    你只需要一個Dockerfile文件即可席纽,也不需要單獨的構(gòu)建腳本撞蚕,只需要運行docker build润梯。

    docker build -t alexellis2/href-counter:latest .
    

    最終的結(jié)果是與前面一樣的極小的結(jié)果,但是復(fù)雜性大大降低,你不需要創(chuàng)建任何中間鏡像寇钉, 也根本不需要將任何文件提取到本地系統(tǒng)舶赔。

    它是如何工作的扫倡?第二個FROM指令使用alpine:latest鏡像作為基礎(chǔ)開始一個新的構(gòu)建階段, COPY --from=0的行將前一個階段的結(jié)果復(fù)制到新的階段竟纳,GO SDK及所有中間產(chǎn)物被拋棄,并沒有保存在最終鏡像中缘挑。

    默認(rèn)情況下揩悄,構(gòu)建階段沒有命名,使用它們的整數(shù)編號引用它們删性,從第一個FORM0開始計數(shù)。 但是你可以使用給FORM指令添加一個as <NAME>為其構(gòu)建階段命名维贺。

    FROM golang:1.7.3 as builder
    WORKDIR /go/src/github.com/alexellis/href-counter/
    RUN go get -d -v golang.org/x/net/html
    COPY app.go    .
    RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
    
    FROM alpine:latest
    RUN apk --no-cache add ca-certificates
    WORKDIR /root/
    COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
    CMD ["./app"]
    
  • 避免安裝不需要的包

    為了降低復(fù)雜性巴帮、減少依賴榕茧、減小文件大小和構(gòu)建時間,應(yīng)該避免安裝額外的或者不必要的軟件包用押。例如,不要在數(shù)據(jù)庫鏡像中包含一個文本編輯器池充。

  • 一個容器只做一件事

    應(yīng)該保證在一個容器中只運行一個進程缎讼。將多個應(yīng)用解耦到不同容器中血崭,保證了容器的橫向擴展和復(fù)用卧惜。例如一個 web 應(yīng)用程序可能包含三個獨立的容器:web應(yīng)用厘灼、數(shù)據(jù)庫、緩存序苏,每個容器都是獨立的鏡像,分開運行围来。但這并不是說一個容器就只跑一個進程匈睁,因為有的程序可能會自行產(chǎn)生其他進程航唆,比如Celery 就可以有很多個工作進程。雖然“每個容器跑一個進程”是一條很好的法則粪狼,但這并不是一條硬性的規(guī)定任岸。我們主要是希望一個容器只關(guān)注意見事情,盡量保持干凈和模塊化困鸥。

    如果容器互相依賴剑按,你可以使用Docker 容器網(wǎng)絡(luò)來把這些容器連接起來艺蝴,我們前面已經(jīng)跟大家講解過 Docker 的容器網(wǎng)絡(luò)模式了。

  • 最小化鏡像層數(shù)

    Docker 17.05 甚至更早 1.10之 前漆诽,盡量減少鏡像層數(shù)是非常重要的锣枝,不過現(xiàn)在的版本已經(jīng)有了一定的改善了:

    • 1.10 以后兰英,只有RUN畦贸、COPY和ADD指令會創(chuàng)建層楞捂,其他指令會創(chuàng)建臨時的中間鏡像趋厉,但是不會直接增加構(gòu)建的鏡像大小了君账。
    • 到了 17.05 版本以后增加了多階段構(gòu)建的支持,允許我們把需要的數(shù)據(jù)直接復(fù)制到最終的鏡像中椭蹄,這就允許我們在中間階段包含一些工具或者調(diào)試信息了净赴,而且不會增加最終的鏡像大小。

    當(dāng)然減少RUN翼馆、COPY金度、ADD的指令仍然是很有必要的审姓,但是我們也需要在 Dockerfile 可讀性(也包括長期的可維護性)和減少層數(shù)之間做一個平衡。

  • 對多行參數(shù)排序

    只要有可能扎筒,就將多行參數(shù)按字母順序排序(比如要安裝多個包時)酬姆。這可以幫助你避免重復(fù)包含同一個包,更新包列表時也更容易骨宠,也更容易閱讀和審查相满。建議在反斜杠符號\ 之前添加一個空格立美,可以增加可讀性。 下面是來自buildpack-deps鏡像的例子:

    RUN apt-get update && apt-get install -y \
      bzr \
      cvs \
      git \
      mercurial \
      subversion
    
  • 構(gòu)建緩存

    在鏡像的構(gòu)建過程中 docker 會遍歷 Dockerfile 文件中的所有指令碌更,順序執(zhí)行。對于每一條指令嘿棘,docker 都會在緩存中查找是否已存在可重用的鏡像旭绒,否則會創(chuàng)建一個新的鏡像

    我們可以使用 docker build --no-cache 跳過緩存

    • ADDCOPY 將會計算文件的 checksum 是否改變來決定是否利用緩存
    • RUN 僅僅查看命令字符串是否命中緩存快压,如 RUN apt-get -y update 可能會有問題

    如一個 node 應(yīng)用,可以先拷貝 package.json 進行依賴安裝坪郭,然后再添加整個目錄脉幢,可以做到充分利用緩存的目的嫌松。

    FROM node:10-alpine as builder
    
    WORKDIR /code
    
    ADD package.json /code
    # 此步將可以充分利用 node_modules 的緩存
    RUN npm install --production
    
    ADD . /code
    
    RUN npm run build 
    
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市液走,隨后出現(xiàn)的幾起案子贾陷,更是在濱河造成了極大的恐慌髓废,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件顶燕,死亡現(xiàn)場離奇詭異冈爹,居然都是意外死亡频伤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進店門惠爽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞬哼,“玉大人坐慰,你說我怎么就攤上這事≡蘖” “怎么了糟港?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵秸抚,是天一觀的道長。 經(jīng)常有香客問我颠放,道長吭敢,這世上最難降的妖魔是什么鹿驼? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮伸头,結(jié)果婚禮上舷蟀,老公的妹妹穿的比我還像新娘野宜。我一直安慰自己,他們只是感情好河胎,可當(dāng)我...
    茶點故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布游岳。 她就那樣靜靜地躺著,像睡著了一般喷户。 火紅的嫁衣襯著肌膚如雪访锻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天河哑,我揣著相機與錄音璃谨,去河邊找鬼遣总。 笑死,一個胖子當(dāng)著我的面吹牛容达,可吹牛的內(nèi)容都是我干的垂券。 我是一名探鬼主播菇爪,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼熙揍!你這毒婦竟也來了届囚?” 一聲冷哼從身側(cè)響起是尖,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤饺汹,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后迎瞧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡蜘醋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了编检。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扰才。...
    茶點故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡衩匣,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出生百,到底是詐尸還是另有隱情柄延,我是刑警寧澤,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布市俊,位于F島的核電站摆昧,受9級特大地震影響蜒程,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜勇吊,卻給世界環(huán)境...
    茶點故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一汉规、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晶伦,春花似錦啄枕、人聲如沸频祝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漓糙。三九已至,卻和暖如春蝗蛙,著一層夾襖步出監(jiān)牢的瞬間捡硅,已是汗流浹背辐棒。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工漾根, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逼蒙。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓寄疏,卻偏偏與公主長得像陕截,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子社搅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,440評論 2 359