iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Modern Web

前端來點 AWS 技能樹!系列 第 9 篇

Day9 - 用多段式 Dockerfile,把 Next.js 打包成 image

  • 分享至 

  • xImage
  •  

昨天我們練習了如何利用 Dockerfile 建立 image,今天來把一個 Next.js 專案打包成 image,並在容器裡跑起來吧!

Image 的層

同一個 image 可以啟動多個 container,每個 container 都有自己的可寫層。在其中一個 container 裡新增檔案,不會影響原本的 image,也不會影響其他 container。
如果用 docker commit 把這個 container 的檔案變更保存成新的 image,再用新的 image 啟動 container,就會帶著這些變更。
不過我們之後不會用 commit,都用 Dockerfile 做 image,因為它可重現、可版本化。
https://ithelp.ithome.com.tw/upload/images/20260923/20179793xYVv7dIVvQ.png
從分層的角度來看,image 的層是唯讀的,container 則會在上面加一層自己的可寫層。下圖的綠色代表 image 的唯讀層,藍色代表 container 的可寫層;commit 後,保存的變更成為新 image 的一部分。
https://ithelp.ithome.com.tw/upload/images/20260923/20179793PxC6gq51TB.png

Image 的層與快取

開始之前,先用昨天的範例看看 Docker 如何重複利用之前的建置結果。

cd ~/ironman-lab/day08
docker history my-site:v2

docker history 可以查看 image 的建置歷史,其中也包含基底 image 原本的紀錄。
接著,在沒有修改 Dockerfile 和檔案的情況下,再 build 一次:

docker build -t my-site:v2 .

觀察輸出中的 COPY 步驟,應該會看到 CACHED,表示 Docker 直接沿用了之前的結果。
接著修改 site/index.html 的文字,我把'v2'改成'v3',存檔後再 build:

docker build -t my-site:v2 .

https://ithelp.ithome.com.tw/upload/images/20260923/20179793GAmVcTEH4u.png
這次 COPY 的來源檔案改變了,因此這一步需要重新執行;前面沒有改變的部分仍可沿用 cache。
Image 是由多個唯讀層疊起來的。像 COPY、RUN 這類指令會產生檔案系統層,而 CMD 等指令則用來設定 image 的執行資訊。
建置時,Docker 會檢查每個步驟能否使用 cache。例如 COPY 會檢查要複製的檔案;如果輸入沒有改變,而且有可用的 cache,就不必重新執行。
當某一步的 cache 失效,後面依賴它的步驟也需要重新建置。因此,安排 Dockerfile 時,可以把不常變動的步驟放前面,經常變動的放後面。等一下安裝 Next.js 專案的依賴時,就會用到這個原則。

用 Dockerfile 打包 Image

開一個新的 Next.js 專案

mkdir -p ~/ironman-lab/day09 && cd ~/ironman-lab/day09
npx create-next-app@latest my-app

先把所有問題設定(TypeScript, ESLint, Tailwind, App Router)都按 Enter(用預設值)

cd my-app
npm run dev

開 http://localhost:3000 看到預設頁,確認後 ^c 關掉

打開 standalone 輸出

在 next.config.ts 原本的 nextConfig 物件中加入 output: 'standalone',其餘內容不要動:

const nextConfig: NextConfig = {
    output: 'standalone',
};

先 build 一次,看看 standalone 做了什麼:

npm run build
ls -a .next/standalone
ls .next/standalone/node_modules | head

-a 可以看到隱藏的檔案或是資料夾
開啟 output: 'standalone' 後,Next.js 會在 build 時追蹤執行需要的檔案,整理到 .next/standalone 裡。
https://ithelp.ithome.com.tw/upload/images/20260923/20179793yxKRn7vD9L.png
裡面可以看到 server.js、package.json、.next,以及只需要用到 node_modules 裡的檔案。這樣打包 image 時,就不必把開發環境整包 node_modules 都帶進去。
不過,standalone 預設不會複製 public 和 .next/static。稍後寫 Dockerfile 時,我們會把它們一起放進 image,讓圖片、CSS 和前端 JavaScript 能正常載入。

加上 .dockerignore

Git 有 .gitignore,Docker 也有 .dockerignore,用來排除不需要送進建置環境的檔案。
在 my-app 根目錄下建立 .dockerignore:

node_modules
.next
.git
.env*

前端專案的 node_modules 通常很大,而且在 Mac 上安裝的套件可能包含無法直接在 Linux 使用的二進位檔。因此,我們會在 Docker 的建置環境裡重新安裝依賴,避免混入本機的版本。
.next 是剛才本機 build 的產物,也先排除,稍後會在 Docker 裡重新 build。.git 是版本控制紀錄,這個範例執行時不需要;.env* 則用來避免把可能含有秘密的環境設定檔一起複製進去。
下一步寫 Dockerfile 時,我們會先安裝依賴並完成 build,再把 standalone 產物和靜態資源複製到最後的 image。

寫 Dockerfile

這份 Dockerfile 分成兩個階段:builder 負責安裝套件並 build;runner 則從 builder 複製執行需要的產物。
COPY --from=builder 就是「從 builder 階段拿檔案過來」。最後啟動容器時,會執行 node server.js,把網站跑起來
在 my-app 根目錄建 Dockerfile:

# 第一階段:安裝套件、build 專案
FROM node:22-alpine AS builder
WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .
RUN npm run build

# 第二階段:拿 build 好的產物來執行
FROM node:22-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public

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

build、跑、看

先把專案打包成 image,命名為 my-app:v1:

docker build -t my-app:v1 .

完成後,查看剛才建立的 image 和大小:

docker image ls my-app

https://ithelp.ithome.com.tw/upload/images/20260923/20179793KBr50tWmwn.png
接著用這個 image 啟動容器:

docker run -d --name app -p 3000:3000 my-app:v1

這裡的 -d 表示在背景執行,--name app 是把容器命名為 app。-p 3000:3000 則把本機的 3000 port 對應到容器的 3000 port,左邊是本機,右邊是容器。
打開 http://localhost:3000,應該會看到剛才的 Next.js 頁面。這次網站是由容器裡的 production server 提供的!
如果前面的 npm run dev 還在執行,記得先按 ^C 關掉,避免占用同一個 port。

看看快取的效果

修改 app/page.tsx 裡的一段文字並存檔。如果專案使用 src 目錄,檔案會在 src/app/page.tsx。
接著重新 build,這次把 image 命名為 my-app:v2:

docker build -t my-app:v2 .

https://ithelp.ithome.com.tw/upload/images/20260923/20179793avuTQc1vXF.png
觀察這次的 build 輸出,RUN npm ci 那一步應該會顯示 CACHED,表示沿用了上一次安裝套件的結果。
因為我們只改了頁面,package.json 和 package-lock.json 都沒有變,所以不需要重新安裝套件;從 COPY . . 開始,才需要重新複製程式碼並執行 build。
這就是先複製依賴清單、安裝套件,再複製程式碼的好處:修改頁面時,可以省下重新安裝套件的時間。
此外,這次只建立了 my-app:v2 image,正在執行的 app 容器仍使用 my-app:v1,因此重新整理瀏覽器還不會看到修改。

收尾

可以停止並移除剛才的容器:

docker rm -f app

這只會移除 app 容器,剛才建立的 my-app:v1 和 my-app:v2 image 都會保留,明天還會用到。

今天我們把 Next.js 專案打包成 image,並成功在容器裡跑起來。透過 standalone 和多段式 Dockerfile,整理出執行網站需要的產物,也實際看到快取如何省下重複安裝套件的時間。
現在,我們已經知道怎麼把應用程式包起來,接下來就要準備一個讓它持續運作的地方。
明天開始進入 AWS,從 EC2 開始,準備一台用來執行容器的雲端主機,往真正的部署再前進一步!


上一篇
Day8 - Dockerfile: 把自己的頁面做成 image
下一篇
Day10 - 專案上 AWS 有哪幾條路
系列文
前端來點 AWS 技能樹! 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言