撰寫 Code Engine 的 Dockerfile

在您將代碼建立到容器映像之前,請先瞭解 Docker 建立在 IBM Cloud® Code Engine 中如何運作的一些基本知識。 然後,查看 Dockerfile 的一些最佳作法,以達成這些目標。 這些範例著重於減少映像大小、改善與 Code Engine 應用程式的互操作性,以及以非 root 使用者的身份執行容器。

當您建立建置配置時,您可以決定要使用兩個可用策略中的哪一個。

  1. Cloud Native Buildpacks 檢查您的原始碼,以偵測您的程式碼所基於的執行環境,以及如何根據您的原始碼建構容器映像檔。 它支援數個程式設計環境。 您可以在 選擇建置策略 下找到支援的清單。

  2. Docker 建置會根據您在 Dockerfile 中的說明方式來建立容器。 然後,Dockerfile 會與您的原始碼一起確定,以建立容器。

雖然您可以對建置使用任一策略,但您可以選擇 Dockerfile,例如:

  • 建置套件不支援您的程式設計環境。
  • 您的專案建置必須在儲存器中安裝其他套件。

Dockerfile 基本概念

Dockerfile 說明如何建置容器。 在 Dockerfile 內,您可以選擇基本映像檔,其中包括建置及執行時期期間所需的必要工具。 您可以將檔案從建置環境定義複製到映像檔,執行指令,定義執行時期行為 (例如環境變數、公開埠),以及設定 ENTRYPOINTENTRYPOINT 是由啟動儲存器時所呼叫的指令所設定。 如需如何指定 Dockerfile 指示的相關資訊,請參閱 Dockerfile 參考資料

在 Code Engine 建置中,您可以定義指向 Git 儲存庫的來源。 依預設,Docker 建置可用的環境定義是 Git 儲存庫的根目錄。 例如,如果您在儲存庫中具有名為 src 的目錄,則可以使用 Dockerfile 中的 COPY 陳述式將此目錄複製到映像檔。 例如,

COPY src /app/src

若要複製整個 Git 儲存庫,請指定下列 COPY 陳述式:

COPY . /app/src

如果您複製整個 Git 儲存庫,但想要排除某些檔案,例如儲存庫的 README.md,那麼您可以新增 .dockerignore 檔案。 使用相同的檔案,也會忽略您在 .gitignore 檔案中指定的檔案及目錄。 透過使用相同的檔案,您可以確保本機執行的建置與 Code Engine 中執行的建置具有相同的可用檔案集。

一律將應用程式檔案複製到根目錄 (/) 的子目錄中,而不是直接複製到根目錄中,以避免與作業系統檔案發生衝突。 當您為應用程式目錄命名時,請勿使用基於 UNIX 的作業系統所保留的 Kubernetes 或 Code Engine 建置,例如 /bin, /dev, /etc, /lib, /proc, /run, /sys, /usr, /var,或 /workspace。 命名應用程式目錄 /app 是最佳作法。

如果您的原始碼儲存庫包含組織在目錄中之不同應用程式的來源,類似於 Code Engine samples repository,則您可以使用子目錄作為環境定義。 在 ibmcloud ce build create 指令中,使用 --context-dir 選項指定子目錄。

如果您的 Dockerfile 不在環境定義目錄中,您可以使用 --dockerfile 引數來指向它。

若要在本端系統上試用 Docker 建置,然後在 Code Engine中建置它,您可以使用 Docker Desktop

減少容器映像檔的大小

減少容器映像檔的大小會在多個層面帶來價值。

  • 在容器登錄中儲存映像檔所需的空間較少。 您可以儲存其他映像檔的配額,並節省金錢。
  • 有時,建置執行需要較少的時間來完成,因為較小的映像檔可以比較大的映像檔更快地傳送至容器登錄。 你又節省了錢
  • 使用映像檔的應用程式或工作啟動速度較快,因為取回映像檔所需的時間較短。 當取回映像檔時,會保留執行應用程式或工作所需的資源,因此您會再次節省金錢。 快速啟動時間對於 Code Engine 中的應用程式特別相關,因為它保證即使應用程式縮減至零,也可接受使用者要求的回應時間。

請查看下列一些最佳作法,以減少建置的大小。

在單一 RUN 陳述式中結合數個指令以減少映像檔大小

在此範例中,您必須在容器映像檔中安裝軟體,例如 Node.js。使用 Node.js 的基本映像檔來建置 Node.js 應用程式。

若要手動安裝 Node.js,您可以使用下列 Dockerfile 範例:

FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y nodejs
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*

在此範例中,使用數個 RUN 陳述式來更新、升級、安裝 Node.js、清除及移除檔案快取。 雖然這個陳述式順序是正確的,但使用多個 RUN 陳述式並不是實作它的最佳方式。 當處理 Dockerfile 時,Dockerfile 中的每一個陳述式都會建立一層。 每一層都包含已建立或更新的檔案,以及已刪除之項目的相關資訊。 儲存器映像檔是所有層的集合。 如果在一個陳述式中新增檔案,並在第二個陳述式中刪除檔案,則產生的影像仍會包含第一個陳述式的層中的檔案,然後在第二個層中將它標示為已刪除。

若要節省磁碟空間,請指定單一 RUN 陳述式,以針對這組指令建立單一層。

FROM ubuntu
RUN apt update && apt upgrade -y && apt install -y nodejs && apt clean && rm -rf /var/lib/apt/lists/\*

若要維護可讀取的 Dockerfile,每個指令一行,請在程式碼中新增換行,

FROM ubuntu
RUN \
    apt update && \
    apt upgrade -y && \
    apt install -y nodejs && \
    apt clean && \
    rm -rf /var/lib/apt/lists/\*

透過使用此類型的陳述式,您可以將儲存器映像檔大小從大約 174 MB 減少至大約 147 MB。 請注意,您的範例大小可能會隨著 Ubuntu 基本映像檔及 Node.js 套件可能變更而不同。

這種類型的最佳作法不僅適用於套件安裝,也適用於類似作業。 在下一個範例中,下載並解壓縮套裝軟體 (同樣為 Node.js) 可能類似於下列範例:

FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y curl
RUN curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz
RUN mkdir /opt/node
RUN tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1
RUN rm /tmp/nodejs.tar.gz
RUN apt remove -y curl
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*

rm 指令會移除下載的 nodejs 套件,但多個 RUN 陳述式會重新建立個別層。 此外,為了下載 nodejs 套件,會暫時安裝 curl 套件。 雖然稍後會移除 curl 套件本身,但隱含安裝的相依關係仍然存在。 一個更完善的 Dockerfile 應如以下範例所示:

FROM ubuntu
RUN \
    apt update && \
    apt upgrade -y && \
    apt install -y curl && \
    curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz && \
    mkdir /opt/node && \
    tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1 && \
    rm /tmp/nodejs.tar.gz && \
    apt remove -y curl && \
    apt auto-remove -y && \
    apt clean && \
    rm -rf /var/lib/apt/lists/\*

相關指令會結合成單一 RUN 陳述式,並新增 apt auto-remove 指令來移除相依關係。 此範例將影像大小從大約 222 MB 減少至大約 162 MB。

使用小型基本映像檔

上述範例使用 Ubuntu 作為基本映像檔。 雖然此映像檔包含許多有用的公用程式,但基本映像檔中的公用程式越多,其大小越大。 此外,透過併入更多公用程式,您會增加可能會遇到安全漏洞的機會,需要您重建映像檔。 若要避免這兩個問題,請使用較小的基本映像檔。 例如,

  • Alpine 是具有小型的正式 Docker 映像檔。 對於程式設計環境 (例如 Java 或 Node.js),您通常會找到以 Alpine為基礎的標籤。

  • 來自 Google Container Tools 的遠端映像檔完全不包含作業系統工具,但適用於不同語言 (例如 Java 和 Node.js) 的必要執行時期環境。

透過建置在 program.js 檔案中執行 Node.js 程式的容器映像檔,來比較這些基本映像檔。 對於 Ubuntu,Dockerfile 類似下列範例:

FROM ubuntu
RUN \
    apt update && \
    apt upgrade -y && \
    apt install -y nodejs && \
    apt clean && \
    rm -rf /var/lib/apt/lists/\*
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]

若為 Alpine,映像檔是直接從 Node.js取得,

FROM node:16-alpine
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]

若為無distro,請使用下列範例:

FROM gcr.io/distroless/nodejs:16
COPY program.js /app/program.js
WORKDIR /app
CMD ["program.js"]

當基於 Ubuntu的映像檔為 147 MB 時,基於 Alpine 的映像檔為 90 MB,基於無 distroless 的映像檔為 94 MB。

不包括來源和建置工具以減少映像檔大小

在先前的 Node.js 型範例中,單一原始檔會新增至容器映像檔。 此範例可以使用單一原始檔,因為不需要編譯。 不過,如果需要編譯,則只會使用建置的必要工具,但不會將它們包含在產生的映像檔中。 例如,指定使用 Maven 作為範例的 Java 應用程式。 編碼不佳的 Dockerfile 看起來類似下列範例:

FROM maven:3-jdk-11-openj9
WORKDIR /app
COPY . /app
RUN mvn package
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/target/java-application-1.0-SNAPSHOT-fat.jar"]

產生的儲存器映像檔包含 Maven 的所有原始碼和所有中間檔 (例如其構件快取、稍後包裝成 JAR 檔的類別檔),以及 Maven 建置工具。 此外,當執行時期需要更小的 Java 執行時期環境 (JRE) 時,也會包含 Java Development Kit (JDK)。 因此,影像大小為 466 MB。

若要縮小影像,請使用稱為「多階段建置」的特性。 Docker 建置中的每一個階段都有自己的基本映像檔,並且可以在其階段中執行指令。 最後會建置最終映像檔,以複製先前階段中的構件。 若要從原始碼建置容器映像檔,一般型樣會有兩個階段:

  1. 使用基本映像檔的建置器暫置,其中包含所有必要的工具,可將原始碼編譯成執行時期的二進位檔。
  2. 將基本映像檔與執行二進位所需的執行時期環境搭配使用的執行時期階段。 建置器階段中的二進位檔會複製到此階段。

對於 Maven 專案,結果類似於下列範例:

FROM maven:3-jdk-11-openj9 AS builder
WORKDIR /app
COPY . /app
RUN mvn package
FROM adoptopenjdk:11-jre-openj9
WORKDIR /app
COPY --from=builder /app/target/java-application-1.0-SNAPSHOT-fat.jar /app/java-application.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/java-application.jar"]

在此範例中,建置器階段使用 Maven 基本映像檔來建置應用程式的 JAR 檔。 執行時期階段使用較小的 JRE 基本映像檔。 透過使用 COPY --from=STAGE_NAME 指令,JAR 檔會從建置器階段複製到執行時期階段。 產生的影像大小為 242 MB,節省大約 48%。

此型樣也可以用於需要執行編譯的其他程式設計語言。

  • 需要建置的 Node 應用程式,例如 Angular 或 React 建置。 在這裡,建置器和執行時期基本映像檔最終可能相同 (兩個節點),但並非所有建置時期構件和來源都需要複製到執行時期映像檔。
  • 將原始碼編譯成在沒有執行時期環境 (例如 Go 或 Rust) 的情況下執行的原生執行檔的任何程式設計語言。

對於產生原生執行檔且完全不需要執行時期環境的那些語言,請針對執行時期階段使用另一個 Docker 功能: 暫存。 暫存可在 FROM 指令中用作基礎,但不是最終儲存器映像檔。 相反地,它會告知 Docker 建置完全不使用基本映像檔。 如果沒有來自基本映像檔的任何作業系統檔案,則結果映像檔可以包含的小到單一檔案: 從建置器階段複製的二進位檔。 請注意,視程式設計語言和您的程式碼而定,您可能必須對編譯器選項進行進一步調整,因為二進位檔可能依賴某些作業系統檔案來存在。

保持您的影像乾淨

當您第一次開發 Dockerfile 並對其運作方式進行除錯時,您可以安裝一些暫時工具,或套用最終容器映像檔中不需要的其他工作負載。 移除這些暫存檔及工作負載可讓您的映像檔保持乾淨、小且更安全。

改善映像檔的開始時間

為了達到最高效率,容器映像檔必須儘快啟動。 對於應用程式,相關度量值是 HTTP 端點可供使用並準備好接受及處理送入的 HTTP 要求所花費的時間。 對於以 Knative 為基礎的 Code Engine 應用程式而言,啟動速度特別重要。 這類應用程式可以配置成在沒有資料流量時縮減為零個執行中實例,因此不會耗用任何資源,也不會花費任何金錢。 當要求進入時,會啟動實例來處理要求。 容器必須儘快啟動,以回應新要求。

若要改善應用程式的啟動,請調查應用程式的實作,並查看您是否可以套用型樣,例如:

  • 獨立起始設定工作的平行化,例如,建立與資料庫的連線,以及讀取配置檔以從環境變數與郵件伺服器進行通訊。
  • 延遲應用程式啟動時不需要的起始設定工作,並改為在第一次需要時執行它們。

此外,當您實作使用架構 (例如 Angular、React 或 Vue) 的 Web 應用程式時,也可以避免常見的陷阱。 其中每一個架構都以 Node.js 與 NPM 為基礎,並包含指令行介面,可讓您輕鬆設定專案。 例如,使用 create-react-app 指令建立的 React 應用程式會設定 package.json 檔案,其中包含部分預先定義的 Script。 其中一個 Script 是 start,它會隨您的 Web 應用程式啟動 Web 伺服器。 您的 Dockerfile 可能類似下列範例:

FROM nodejs:16-alpine
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "run", "start"]

當此類型的 Dockerfile 運作時,它並不快速,因為每當呼叫 npm run start 指令時,就會編譯然後啟動應用程式。 對於超出小樣本大小的應用程式而言,此延遲尤其明顯。 正確的方法是在建置時期編譯應用程式,並只在啟動時才提供它。

FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]

您會再次看到建置器和執行時期兩階段型樣。 另請注意,更新的範例使用不同的埠 8080。 當此範例與任何其他埠搭配使用時,8080 是 Code Engine 應用程式的預設埠。 此外,透過使用已編譯的建置,node_modules 中安裝的所有來源及工具都不會包含在最終儲存器映像檔中,這會減少其大小 281-97 MB。

以非 root 使用者身分執行容器

精心設計的系統遵循最少專用權的原則-應用程式或使用者只會取得執行特定動作所需的那些專用權。 在 Code Engine中,您執行應用程式伺服器或某個批次邏輯,通常不需要系統的管理存取權。 因此,它不得在其儲存器中以 root 身分執行。 良好的作法是使用已定義的使用者來設定容器映像檔,並以非 root 使用者身分執行。 例如,根據我們先前的實務範例,

FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]

Dockerfile 會使用 USER 指令來指定它要以使用者和群組 1100 身分執行。 請注意,此指令不會隱含地在儲存器映像檔中建立具名使用者和群組。 通常可以接受此結構,但如果您的應用程式邏輯需要使用者及其起始目錄存在,則您必須明確地建立使用者和群組:

FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve && \
    addgroup nonroot --gid 1100 && \
    adduser nonroot --ingroup nonroot --uid 1100 --home /home/nonroot --disabled-password
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]

執行時期階段中的 RUN 指令已延伸,可呼叫 addgroupadduser 指令,以建立群組及具有起始目錄的使用者。