跳至主要内容

4.14 Express.js 應用程式的多階段建置

這次挑選了一個熱門的後端框架來作為多階段建置的練習對象,不繼續選用前端框架,如 Next.js 以及 Nust.js 的原因,是因為這些框架的多階段建置會根據不同框架有不同的寫法,這邊主要還是先練習直譯式的程式語言在多階段建置時的概念比較重要。

這次的練習範例我也會放在本書的 GitHub 儲存庫中 ch-04 的 express-js-multi-stage 中。

接著我們進入到資料夾內,並且建置映像檔。

$ docker image build --tag express-example .
[+] Building 7.2s (10/10) FINISHED
=> [internal] load build definition from Dockerfile 0.1s
=> => transferring dockerfile: 144B 0.0s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:16-alpine 0.0s
=> [auth] library/node:pull token for registry-1.docker.io 2.5s
=> [1/4] FROM docker.io/library/node:16-alpine@sha256:2c405....... 0.0s
=> [internal] load build context 0.1s
=> => transferring context: 59.16B 0.1s
=> CACHED [2/4] WORKDIR /app 0.0s
=> [3/4] COPY . /app/ 0.0s
=> [4/4] RUN yarn install 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:f01f0812beeb370ddc5dee3d9290cd062315.... 0.0s
=> => naming to docker.io/library/express-example 0.0s

接著將建置完的映像檔運行成容器,先確認這個映像檔是可以使用的。

$ docker container run --publish 3000:3000 --detach express-example # 不換行
3a31d044b4b257634b1499d7d75864022774a46e...

接著打開瀏覽器輸入 http://localhost:3000,會看到『 Hello,這是一個用 Express.js 建置的 WebServer 』字樣。

確定可以使用之後,來看看多階段建置前映像檔的大小:

$ docker image list --filter=reference='express-example'
REPOSITORY TAG IMAGE ID CREATED SIZE
express-example latest f01f0812beeb 5 minutes ago 120MB

接著打開資料夾內的 Dockerfile,並且開始嘗試將目前的寫法轉成多階段建置。

如同前面所提過的,要寫好 Dockerfile 還是得對於使用的程式語言有基本的理解,在 JavaScript 的世界內,有 npm 以及 yarn 兩種套件管理工具,而最主要的的運作模式就是透過套件管理工具安裝好需要的套件,並且引入作為使用,但也因為不需要編譯的關係,在撰寫上會簡單很多。

所以開始前的思考方向應該是,先在第一個階段把需要的套件安裝完,並且把安裝完的套件放置到第二個階段,就能夠減少執行時不需要的工具。

首先先把第一階段寫出來,需要進入一個叫做 app 的檔案目錄,並且將紀錄著使用套件的 package.json 以及 yarn.lock 兩個檔案複製到映像檔中,並且透過 yarn install 的指令來安裝需要的套件。

FROM node:16-alpine AS builder

COPY package.json yarn.lock ./

RUN yarn install

這邊就完成了第一階段,接著需要複製第一階段安裝完全部的套件包 node_moduels 到第二階段。

FROM node:16-alpine AS builder

COPY package.json yarn.lock ./

RUN yarn install


-----階段分界示意線----- # 不要寫進 Dockerfile
FROM node:16-alpine

EXPOSE 3000

CMD ["node", "app.js"]

WORKDIR /app

COPY --from=builder /node_modules /app/node_modules

COPY app.js ./ <-- 只複製了主要的檔案

可以看到第二階段,只複製了 app.js 這個主要的檔案到映像檔內,其它對於執行程式不需要的檔案都不會被複製進來。

$ docker image build --tag express-min-example .
[+] Building 8.0s (11/11) FINISHED
=> [internal] load build definition from Dockerfile 0.5s
=> => transferring dockerfile: 37B 0.1s
=> [internal] load .dockerignore 0.1s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:16-alpine 1.1s
=> [internal] load build context 0.2s
=> => transferring context: 90B 0.1s
=> CACHED [builder 1/3] FROM docker.io/library/node:16-alpine@s... 0.0s
=> CACHED [stage-1 2/4] WORKDIR /app 0.0s
=> [builder 2/3] COPY package.json yarn.lock ./ 0.1s
=> [builder 3/3] RUN yarn install 4.9s
=> [stage-1 3/4] COPY --from=builder /node_modules /app/node_modules 0.3s
=> [stage-1 4/4] COPY app.js ./ 0.1s
=> exporting to image 0.3s
=> => exporting layers 0.3s
=> => writing image sha256:f01f0812beeb370ddc5dee3d9290cd062315.... 0.0s
=> => naming to docker.io/library/express-example 0.0s

接著一樣將其運行成容器,試試看是不是多階段建置也能夠達到相同的效果。

$ docker container rm --force $(docker container ls -aq)
# 先清掉所有容器,避免 port 衝突

$ docker container run --publish 3000:3000 --detach express-min-example # 不換行
6e945dd81d8e47acf1adf5e3944243264f049d6394c82f.....

再來一樣打開瀏覽器輸入 http://localhost:3000,就會看到和上次啟動容器時一模一樣的畫面,證明這個方式建置的映像檔可以達到一樣的目的。

接著就是要揭曉映像檔容器大小的時刻了,先打預防針,差距真的會很小,直譯式的程式語言需要套件在一旁隨時待命,導致其能夠縮小的空間真的不多。

$ docker image list --filter=reference='express-*'
REPOSITORY TAG IMAGE ID CREATED SIZE
express-min-example latest 27505665627 7 minutes ago 117MB
express-example latest e336ff151fd 20 minutes ago 120MB

是的,你沒看錯,真的就只差了 3 個 MB,但某部分也是因為這個例子只是拿來練習多階段建置,而且內容很少;如果有某些套件是需要編譯的,那就可以讓容量差距更大。

想必對於多階段建置映像檔也有了更深一層的概念,不外乎就是把平常的安裝工具、相依套件、編譯等等的工作放在前面的階段,而最後的階段通常都只是拿來運行應用程式,以及滿足應用程式所需的最低要求。

而這樣的方式其實也可以透過 .dockerignore 這個檔案來處理,效果就像 .gitignore 一樣,會在建置映像檔的時候自動忽略標註在內的檔案。