怎樣編寫正確、高效的 Dockerfile

基礎鏡像

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=//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"]
Tags: