怎樣編寫正確、高效的 Dockerfile

2022-09-23 18:00:32

基礎映象

FROM 基礎映象
基礎映象的選擇非常關鍵:

  • 如果關注的是映象的安全和大小,那麼一般會選擇 Alpine;
  • 如果關注的是應用的執行穩定性,那麼可能會選擇 Ubuntu、Debian、CentOS。

構建上下文與.gitignore

真正的映象構建工作是由伺服器端的「Docker daemon」來完成的,所以「docker」使用者端就只能把「構建上下文」目錄打包上傳(顯示資訊 Sending build context to Docker daemon ),這樣伺服器才能夠獲取原生的這些檔案。
「構建上下文」其實與 Dockerfile 並沒有直接的關係,它其實指定了要打包進映象的一些依賴檔案。而 COPY 命令也只能使用基於「構建上下文」的相對路徑,因為「Docker daemon」看不到本地環境,只能看到打包上傳的那些檔案。但這個機制也會導致一些麻煩,如果目錄裡有的檔案(例如 readme/.git/.svn 等)不需要拷貝進映象,docker 也會一股腦地打包上傳,效率很低。為了避免這種問題,你可以在「構建上下文」目錄裡再建立一個 .dockerignore 檔案,語法與 .gitignore 類似,排除那些不需要的檔案。

# docker ignore
*.swp
*.sh
*.git

指令

每個指令都會生成一個映象層,所以 Dockerfile 裡最好不要濫用指令,儘量精簡合併,否則會太多的層會導致映象臃腫不堪。

COPY

在本機上開發測試時會產生一些原始碼、設定等檔案,需要打包進映象裡,可以使用 COPY 命令,它的用法和 Linux 的 cp 差不多,不過拷貝的原始檔必須是「構建上下文」路徑裡的,不能隨意指定檔案。也就是說,如果要從本機向映象拷貝檔案,就必須把這些檔案放到一個專門的目錄,然後在 docker build 裡指定「構建上下文」到這個目錄才行。

COPY ./a.txt  /tmp/a.txt    # 把構建上下文裡的a.txt拷貝到映象的/tmp目錄
COPY /etc/hosts  /tmp       # 錯誤!不能使用構建上下文之外的檔案

RUN

Dockerfile 裡最重要的一個指令 RUN ,它可以執行任意的 Shell 命令,比如更新系統、安裝應用、下載檔案、建立目錄、編譯程式等等,實現任意的映象構建步驟,非常靈活。
RUN 通常會是 Dockerfile 裡最複雜的指令,會包含很多的 Shell 命令,但 Dockerfile 裡一條指令只能是一行,所以有的 RUN 指令會在每行的末尾使用續行符 \,命令之間也會用 && 來連線,這樣保證在邏輯上是一行

RUN apt-get update \
    && apt-get install -y \
        build-essential \
        curl \
        make \
        unzip \
    && cd /tmp \
    && curl -fSL xxx.tar.gz -o xxx.tar.gz\
    && tar xzf xxx.tar.gz \
    && cd xxx \
    && ./config \
    && make
    && make clean

把這些 Shell 命令集中到一個指令碼檔案裡,用 COPY 命令拷貝進去再用 RUN 來執行:

COPY setup.sh  /tmp/                # 拷貝指令碼到/tmp目錄

RUN cd /tmp && chmod +x setup.sh \  # 新增執行許可權
    && ./setup.sh && rm setup.sh    # 執行指令碼然後再刪除

RUN 指令實際上就是 Shell 程式設計,如果你對它有所瞭解,就應該知道它有變數的概念,可以實現引數化執行,這在 Dockerfile 裡也可以做到,需要使用兩個指令 ARG 和 ENV。

ARG IMAGE_BASE="node"
ARG IMAGE_TAG="alpine"

ENV PATH=$PATH:/tmp
ENV DEBUG=OFF

EXPOSE

EXPOSE,它用來宣告容器對外服務的埠號,對現在基於 Node.js、Tomcat、Nginx、Go 等開發的微服務系統來說非常有用:

EXPOSE 443           # 預設是tcp協定
EXPOSE 53/udp        # 可以指定udp協定

Docker多階段構建

什麼是多階段構建

多階段構建指在Dockerfile中使用多個FROM語句,每個FROM指令都可以使用不同的基礎映象,並且是一個獨立的子構建階段。使用多階段構建打包Java/GO應用具有構建安全、構建速度快、映象檔案體積小等優點。

映象構建的通用問題

映象構建服務使用Dockerfile來幫助使用者構建最終映象,但在具體實踐中,存在一些問題:

  • Dockerfile編寫有門檻
    開發者(尤其是Java)習慣了語言框架的編譯便利性,不知道如何使用Dockerfile構建應用映象。

  • 映象容易臃腫
    構建映象時,開發者會將專案的編譯、測試、打包構建流程編寫在一個Dockerfile中。每條Dockerfile指令都會為映象新增一個新的圖層,從而導致映象層次深,映象檔案體積特別大

  • 存在原始碼洩露風險
    打包映象時,原始碼容易被打包到映象中,從而產生原始碼洩漏的風險。

多階段構建優勢

針對Java這類的編譯型語言,使用Dockerfile多階段構建,具有以下優勢:

  • 保證構建映象的安全性
    當您使用Dockerfile多階段構建映象時,需要在第一階段選擇合適的編譯時基礎映象,進行程式碼拷貝、專案依賴下載、編譯、測試、打包流程。在第二階段選擇合適的執行時基礎映象,拷貝基礎階段生成的執行時依賴檔案。最終構建的映象將不包含任何原始碼資訊。

  • 優化映象的層數和體積
    構建的映象僅包含基礎映象和編譯製品,映象層數少,映象檔案體積小。

  • 提升構建速度
    使用構建工具(Docker、Buildkit等),可以並行執行多個構建流程,縮短構建耗時。

使用多階段構建Dockerfile

以Java Maven專案為例,在Java Maven專案中新建Dockerfile檔案,並在Dockerfile檔案新增以下內容。

該Dockerfile檔案使用了二階段構建。

  1. 第一階段:選擇Maven基礎映象(Gradle型別也可以選擇相應Gradle基礎映象)完成專案編譯,拷貝原始碼到基礎映象並執行RUN命令,從而構建Jar包。
  2. 第二階段:拷貝第一階段生成的Jar包到OpenJDK映象中,設定CMD執行命令。
# First stage: complete build environment
FROM maven:3.5.0-jdk-8-alpine AS builder

# add pom.xml and source code
ADD ./pom.xml pom.xml
ADD ./src src/

# package jar
RUN mvn clean package

# Second stage: minimal runtime environment
From openjdk:8-jre-alpine

# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar

EXPOSE 8080

CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]

go專案兩階段構建範例

# First stage: complete build environment
FROM golang:1.17 AS builder
ENV GOSUMDB=off
ENV GOPROXY=https://goproxy.cn,direct
WORKDIR /go/src

# compile
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -o httpserver main.go

# Second stage: minimal runtime environment
# copy binary file
FROM alpine:3.9.4
RUN mkdir /app \
     # 紀錄檔路徑,務必和應用中紀錄檔路徑保持一致
    && mkdir /var/log/httpserver \
    && addgroup -g 10001 httpserver \
    && adduser -S -u 10001 -G httpserver httpserver
COPY --from=builder /go/src/httpserver /app/httpserver
COPY --from=builder /go/src/cert /app/cert/

RUN chown -R 10001:10001 /app \
    && chown -R 10001:10001 /var/log/httpserver

USER httpserver
WORKDIR /app
ENTRYPOINT ["./httpserver"]