看完這篇,再也不用擔(dān)心不會寫dockerfile了

Dockerfile是Docker用來構(gòu)建鏡像的文本文件,包括自定義的指令和格式??梢酝ㄟ^docker build命令從Dockerfile中構(gòu)建鏡像。用戶可以通過統(tǒng)一的語法命令來根據(jù)需求進行配置,通過這份統(tǒng)一的配置文件,在不同的文件上進行分發(fā),需要使用時就可以根據(jù)配置文件進行自動化構(gòu)建,這解決了開發(fā)人員構(gòu)建鏡像的復(fù)雜過程。

Dockerfile的使用

Dockerfile描述了組裝對象的步驟,其中每條指令都是單獨運行的。除了FROM指令,其他每條命令都會在上一條指令所生成鏡像的基礎(chǔ)上執(zhí)行,執(zhí)行完后會生成一個新的鏡像層,新的鏡像層覆蓋在原來的鏡像之上從而形成了新的鏡像。Dockerfile所生成的最終鏡像就是在基礎(chǔ)鏡像上面疊加一層層的鏡像層組建的。

Dockerfile指令

Dockerfile的基本格式如下:

# Comment
INSTRUCTION arguments

在Dockerfile中,指令(INSTRUCTION)不區(qū)分大小寫,但是為了與參數(shù)區(qū)分,推薦大寫。
Docker會順序執(zhí)行Dockerfile中的指令,第一條指令必須是FROM指令,它用于指定構(gòu)建鏡像的基礎(chǔ)鏡像。在Dockerfile中以#開頭的行是注釋,而在其他位置出現(xiàn)的#會被當(dāng)成參數(shù)。

Dockerfile中的指令有FROM、MAINTAINER、RUN、CMD、EXPOSE、ENV、ADD、COPY、ENTRYPOING、VOLUME、USER、WORKDIR、ONBUILD,錯誤的指令會被忽略。下面將詳細講解一些重要的Docker指令。

FROM

格式: FROM <image> 或者 FROM <image>:<tag>

FROM指令的功能是為后面的指令提供基礎(chǔ)鏡像,因此Dockerfile必須以FROM指令作為第一條非注釋指令。從公共鏡像庫中拉取鏡像很容易,基礎(chǔ)鏡像可以選擇任何有效的鏡像。
在一個Dockerfile中FROM指令可以出現(xiàn)多次,這樣會構(gòu)建多個鏡像。tag的默認值是latest,如果參數(shù)image或者tag指定的鏡像不存在,則返回錯誤。

ENV

格式: ENV <key> <value> 或者 ENV <key>=<value> ...

ENV指令可以為鏡像創(chuàng)建出來的容器聲明環(huán)境變量。并且在Dockerfile中,ENV指令聲明的環(huán)境變量會被后面的特定指令(即ENV、ADD、COPY、WORKDIR、EXPOSE、VOLUME、USER)解釋使用。

其他指令使用環(huán)境變量時,使用格式為$variable_name或者${variable_name}。如果在變量面前添加斜杠\可以轉(zhuǎn)義。如\$foo或者\${foo}將會被轉(zhuǎn)換為$foo${foo},而不是環(huán)境變量所保存的值。另外,ONBUILD指令不支持環(huán)境替換。

COPY

格式: COPY <src> <dest>

COPY指令復(fù)制<src>所指向的文件或目錄,將它添加到新鏡像中,復(fù)制的文件或目錄在鏡像中的路徑是<dest>。<src>所指定的源可以有多個,但必須是上下文根目錄中的相對路徑。
不能只用形如 COPY ../something /something這樣的指令。此外,<src>可以使用通配符指向所有匹配通配符的文件或目錄,例如,COPY home* /mydir/ 表示添加所有以"hom"開頭的文件到目錄/mydir/中。

<dest>可以是文件或目錄,但必須是目標鏡像中的絕對路徑或者相對于WORKDIR的相對路徑(WORKDIR即Dockerfile中WORKDIR指令指定的路徑,用來為其他指令設(shè)置工作目錄)。
<dest>以反斜杠/結(jié)尾則其指向的是目錄;否則指向文件。<src>同理。若<dest>是一個文件,則<src>的內(nèi)容會被寫到<dest>中;否則<src>指向的文件或目錄中的內(nèi)容會被復(fù)制添加到<dest>目錄中。
當(dāng)<src>指定多個源時,<dest>必須是目錄。如果<dest>不存在,則路徑中不存在的目錄會被創(chuàng)建。

ADD

格式:ADD <src> <dest>

ADD與COPY指令在功能上很相似,都支持復(fù)制本地文件到鏡像的功能,但ADD指令還支持其他功能。<src>可以是指向網(wǎng)絡(luò)文件的URL,此時若<dest>指向一個目錄,則URL必須是完全路徑,這樣可以獲得網(wǎng)絡(luò)文件的文件名filename,該文件會被復(fù)制添加到<dest>/<filename>
比如 ADD http://example.com/config.property / 會創(chuàng)建文件/config.property。

<src>還可以指向一個本地壓縮歸檔文件,該文件會在復(fù)制到容器時會被解壓提取,如ADD sxample.tar.xz /。但是若URL中的文件為歸檔文件則不會被解壓提取。

ADD 和 COPY指令雖然功能相似,但一般推薦使用COPY,因為COPY只支持本地文件,相比ADD而言,它更加透明。

EXPOSE

格式: EXPOSE <port> [<port>/<protocol>...]

EXPOSE指令通知Docker該容器在運行時偵聽指定的網(wǎng)絡(luò)端口??梢灾付ǘ丝谑莻陕燭CP還是UDP,如果未指定協(xié)議,則默認值為TCP。
這個指令僅僅是聲明容器打算使用什么端口而已,并不會自動在宿主機進行端口映射,可以在運行的時候通過docker -p指定。

EXPOSE 80/tcp
EXPOSE 80/udp

USER

格式: USER <user>[:<group] 或者 USER <UID>[:<GID>]

USER指令設(shè)置了user name和user group(可選)。在它之后的RUN,CMD以及ENTRYPOINT指令都會以設(shè)置的user來執(zhí)行。

WORKDIR

格式: WORKDIR /path/to/workdir

WORKDIR指令設(shè)置工作目錄,它之后的RUN、CMD、ENTRYPOINT、COPY以及ADD指令都會在這個工作目錄下運行。如果這個工作目錄不存在,則會自動創(chuàng)建一個。
WORKDIR指令可在Dockerfile中多次使用。如果提供了相對路徑,則它將相對于上一個WORKDIR指令的路徑。例如

WORKDIR /a
WORKDIR b
WORKDIR c
RUN pwd

輸出結(jié)果是 /a/b/c

RUN

格式1: RUN <command> (shell格式)
格式2: RUN ["executable", "param1", "param2"] (exec格式,推薦使用)

RUN指令會在前一條命令創(chuàng)建出的鏡像的基礎(chǔ)上創(chuàng)建一個容器,并在容器中運行命令,在命令結(jié)束運行后提交容器為新鏡像,新鏡像被Dockerfile中的下一條指令使用。

RUN指令的兩種格式表示命令在容器中的兩種運行方式。當(dāng)使用shell格式時,命令通過/bin/sh -c運行。
當(dāng)使用exec格式時,命令是直接運行的,容器不調(diào)用shell程序,即容器中沒有shell程序。
exec格式中的參數(shù)會被當(dāng)成JSON數(shù)組被Docker解析,故必須使用雙引號而不能使用單引號。因為exec格式不會在shell中執(zhí)行,所以環(huán)境變量的參數(shù)不會被替換。

比如執(zhí)行RUN ["echo", "$HOME"]指令時,HOME不會做變量替換。如果希望運行shell程序,指令可以寫成 `RUN ["/bin/bash", "-c", "echo", "HOME"]`。

CMD

CMD指令有3種格式。

格式1:CMD <command> (shell格式)
格式2:CMD ["executable", "param1", "param2"] (exec格式,推薦使用)
格式3:CMD ["param1", "param2"] (為ENTRYPOINT指令提供參數(shù))

CMD指令提供容器運行時的默認值,這些默認值可以是一條指令,也可以是一些參數(shù)。一個Dockerfile中可以有多條CMD指令,但只有最后一條CMD指令有效。
CMD ["param1", "param2"]格式是在CMD指令和ENTRYPOINT指令配合時使用的,CMD指令中的參數(shù)會添加到ENTRYPOING指令中.使用shell和exec格式時,命令在容器中的運行方式與RUN指令相同。

不同之處在于,RUN指令在構(gòu)建鏡像時執(zhí)行命令,并生成新的鏡像;CMD指令在構(gòu)建鏡像時并不執(zhí)行任何命令,而是在容器啟動時默認將CMD指令作為第一條執(zhí)行的命令。如果用戶在命令行界面運行docker run命令時指定了命令參數(shù),則會覆蓋CMD指令中的命令。

ENTRYPOINT

ENTRYPOINT指令有兩種格式。

格式1:ENTRYPOINT <command> (shell格式)
格式2:ENTRYPOINT ["executable", "param1", "param2"] (exec格式,推薦格式)

ENTRYPOINT指令和CMD指令類似,都可以讓容器在每次啟動時執(zhí)行相同的命令,但它們之間又有不同。一個Dockerfile中可以有多條ENTRYPOINT指令,但只有最后一條ENTRYPOINT指令有效。

當(dāng)使用Shell格式時,ENTRYPOINT指令會忽略任何CMD指令和docker run命令的參數(shù),并且會運行在bin/sh -c中。這意味著ENTRYPOINT指令進程為bin/sh -c的子進程,進程在容器中的PID將不是1,且不能接受Unix信號。即當(dāng)使用docker stop <container>命令時,命令進程接收不到SIGTERM信號。

推薦使用exec格式,使用此格式時,docker run傳入的命令參數(shù)會覆蓋CMD指令的內(nèi)容并且附加到ENTRYPOINT指令的參數(shù)中。從ENTRYPOINT的使用中可以看出,CMD可以是參數(shù),也可以是指令,而ENTRYPOINT只能是命令;另外,docker run命令提供的運行命令參數(shù)可以覆蓋CMD,但不能覆蓋ENTRYPOINT。

Dockerfile實踐心得

使用標簽

給鏡像打上標簽,有利于幫助了解進鏡像功能

謹慎選擇基礎(chǔ)鏡像

選擇基礎(chǔ)鏡像時,盡量選擇當(dāng)前官方鏡像庫的肩寬,不同鏡像的大小不同,目前Linux鏡像大小由如下關(guān)系:

busybox < debian < centos < ubuntu

同時在構(gòu)建自己的Docker鏡像時,只安裝和更新必須使用的包。此外相比Ubuntu鏡像,更推薦使用Debian鏡像,因為它非常輕量級(目前其大小是在100MB以下),并且仍然是一個完整的發(fā)布版本。

充分利用緩存

Docker daemon會順序執(zhí)行Dockerfile中的指令,而且一旦緩存失效,后續(xù)命令將不能使用緩存。為了有效地利用緩存,需要保證指令的連續(xù)性,盡量將所有Dockerfile文件相同的部分都放在前面,而將不同的部分放到后面。

正確使用ADD與COPY命令

當(dāng)在Dockerfile中的不同部分需要用到不同的文件時,不要一次性地將這些文件都添加到鏡像中去,而是在需要時添加,這樣也有利于重復(fù)利用docker緩存。
另外考慮到鏡像大小問題,使用ADD指令去獲取遠程URL中的壓縮包不是推薦的做法。應(yīng)該使用RUN wget或RUN curl代替。這樣可以刪除解壓后不在需要的文件,并且不需要在鏡像中在添加一層。

錯誤做法:

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

RUN指令

在使用較長的RUN指令時可以使用反斜杠\分隔多行。大部分使用RUN指令的常見是運行apt-wget命令,在該場景下請注意以下幾點。

  1. 不要在一行中單獨使用指令RUN apt-get update。當(dāng)軟件源更新后,這樣做會引起緩存問題,導(dǎo)致RUN apt-get install指令運行失敗。所以,RUN apt-get update和RUN apt-get install應(yīng)該寫在同一行。比如 RUN apt-get update && apt-get install -y package-1 package-2 package-3

  2. 避免使用指令RUN apt-get upgrade 和 RUN apt-get dist-upgrade。因為在一個無特權(quán)的容器中,一些必要的包會更新失敗。如果需要更新一個包(如package-1),直接使用命令RUN apt-get install -y package-1。

CMD和ENTRYPOINT命令

CMD和ENTRYPOINT命令指定是了容器運行的默認命令,推薦二者結(jié)合使用。使用exec格式的ENTRYPOINT指令設(shè)置固定的默認命令和參數(shù),然后使用CMD指令設(shè)置可變的參數(shù)。

比如下面這個例子:

FROM busybox
WORKDIR /app
COPY run.sh /app
RUN chmod +x run.sh
ENTRYPOINT ["/app/run.sh"]
CMD ["param1"]

run.sh內(nèi)容如下:

#!/bin/sh
echo "$@"

運行后輸出結(jié)果為param1, Dockerfile中CMD和ENTRYPOINT的順序不重要(CMD寫在ENTRYPOINT前后都可以)。

當(dāng)在windows系統(tǒng)下build dockerfile你可能會遇到這個問題

standard_init_linux.go:207: exec user process caused "no such file or directory"

這是因為sh文件的fileformat是dos,這里需要修改為unix,不需要下載額外的工具,一般我們機器上安裝了git會自帶git bash,進入git bash,使用vi 編輯,在命令行模式下修改(:set ff=unix)。

不要再Dockerfile中做端口映射

使用Dockerfile的EXPOSE指令,雖然可以將容器端口映射在主機端口上,但會破壞Docker的可移植性,且這樣的鏡像在一臺主機上只能啟動一個容器。所以端口映射應(yīng)在docker run命令中用-p 參數(shù)指定。

# 不要再Dockerfile中做如下映射
EXPOSE 80:8080

# 僅暴露80端口,需要另做映射
EXPOSE 80

實踐Dockerfile的寫法

Java 服務(wù)的DockerFile

FROM openjdk:8-jre-alpine
ENV spring_profiles_active=dev
ENV env_java_debug_enabled=false
EXPOSE 8080
WORKDIR /app
ADD target/smcp-web.jar /app/target/smcp-web.jar
ADD run.sh /app
ENTRYPOINT ./run.sh

可以看到基礎(chǔ)鏡像是openjdk,然后設(shè)置了兩個環(huán)境變量,服務(wù)訪問端口是9090(意味著springboot應(yīng)用中指定了server.port=8080),設(shè)置了工作目錄是/app。通過ENTRYPOINT設(shè)定了啟動鏡像時要啟動的命令(./run.sh)。這個腳本中的內(nèi)容如下:

#!/bin/sh
# Set debug options if required
if [ x"${env_java_debug_enabled}" != x ] && [ "${env_java_debug_enabled}" != "false" ]; then
    java_debug_args="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"
fi

# ex: env_jvm_flags="-Xmx1200m -XX:MaxRAM=1500m" for production
java $java_debug_args $env_jvm_flags -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -jar target/smcp-web.jar

如果我們要指定jvm的一些參數(shù),可以通過在環(huán)境變量中設(shè)置env_jvm_flags來指定。

Maven Dockerfile

maven的Dockerfile也寫的很好,這里我發(fā)上來也給大家參考下

FROM openjdk:8-jdk

ARG MAVEN_VERSION=3.6.3
ARG USER_HOME_DIR="/root"
ARG SHA=c35a1803a6e70a126e80b2b3ae33eed961f83ed74d18fcd16909b2d44d7dada3203f1ffe726c17ef8dcca2dcaa9fca676987befeadc9b9f759967a8cb77181c0
ARG BASE_URL=https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries

RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
  && curl -fsSL -o /tmp/apache-maven.tar.gz ${BASE_URL}/apache-maven-${MAVEN_VERSION}-bin.tar.gz \
  && echo "${SHA}  /tmp/apache-maven.tar.gz" | sha512sum -c - \
  && tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
  && rm -f /tmp/apache-maven.tar.gz \
  && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"

COPY mvn-entrypoint.sh /usr/local/bin/mvn-entrypoint.sh
COPY settings-docker.xml /usr/share/maven/ref/

ENTRYPOINT ["/usr/local/bin/mvn-entrypoint.sh"]
CMD ["mvn"]

可以看到它是基于openjdk這個基礎(chǔ)鏡像來創(chuàng)建的,先去下載maven的包,然后進行了安裝。 然后又設(shè)置了MAVEN_HOME和MAVEN_CONFIG這兩個環(huán)境變量,最后通過mvn-entrypoing.sh來進行了啟動。

前端服務(wù)的兩階段構(gòu)建

我有一個前端服務(wù),目錄結(jié)構(gòu)如下:

$ ls frontend/
myaccount/  resources/  third_party/

myaccount目錄下是放置的js,vue等,resources放置的是css,images等。third_party放的是第三方應(yīng)用。

這里采用了兩階段構(gòu)建,即采用上一階段的構(gòu)建結(jié)果作為下一階段的構(gòu)建數(shù)據(jù)

FROM node:alpine as builder
WORKDIR '/build'
COPY myaccount ./myaccount
COPY resources ./resources
COPY third_party ./third_party

WORKDIR '/build/myaccount'

RUN npm install
RUN npm rebuild node-sass
RUN npm run build

RUN ls /build/myaccount/dist

FROM nginx
EXPOSE 80
COPY --from=builder /build/myaccount/dist /usr/share/nginx/html

需要注意結(jié)尾的 --from=builder這里和開頭是遙相呼應(yīng)的。

總結(jié)

我相信看完dockerfile指令,你看任何一個dockerfile應(yīng)該都沒有太大問題,不記得的命令回來翻一下就行了。如果你覺得還可以,關(guān)注下喲。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容