Dockerfile 實戰:容器化你的應用

在上一章中,我們安裝了 Docker 並理解了容器化的基本概念。現在,讓我們實際動手,將一個真實的應用程式打包成 Docker Image!

什麼是 Dockerfile?

Dockerfile 是一個純文字腳本檔案,裡面包含了一系列的「指令 (Instructions)」,告訴 Docker Engine 該如何逐步建立一個 Image。

你可以把 Dockerfile 想像成「食譜」:

  • 食譜告訴你要準備哪些食材(基底 Image)
  • 食譜告訴你料理的步驟(依序執行的指令)
  • 最後產出成品(Docker Image)

Dockerfile 常用指令一覽

在開始寫 Dockerfile 之前,我們先快速了解幾個最重要的指令:

| 指令 | 功能 | 範例 | |------|------|------| | FROM | 指定基底 Image(必填) | FROM node:20-alpine | | WORKDIR | 設定工作目錄 | WORKDIR /app | | COPY | 複製檔案到 Image 中 | COPY package.json ./ | | RUN | 在建置過程中執行指令 | RUN npm ci | | ENV | 設定環境變數 | ENV NODE_ENV=production | | EXPOSE | 宣告容器要暴露的埠 | EXPOSE 3000 | | CMD | 容器啟動時預設執行的指令 | CMD ["node", "server.js"] | | ENTRYPOINT | 容器啟動時的進入點(與 CMD 不同) | ENTRYPOINT ["node"] | | HEALTHCHECK | 定義健康檢查指令 | HEALTHCHECK CMD curl ... | | USER | 指定執行使用者 | USER node |

實戰:容器化一個 Next.js 應用

我們以一個常見的 Next.js 應用為例,從零開始撰寫 Dockerfile。

專案結構

my-next-app/
├── package.json
├── next.config.js
├── public/
├── src/
│   ├── app/
│   └── components/
└── Dockerfile   ← 我們要新增的檔案

第一步:基礎版本(初階 Dockerfile)

在專案根目錄建立一個 Dockerfile(沒有副檔名):

# 使用 Node.js 20 作為基底 Image
FROM node:20

# 設定容器內的工作目錄
WORKDIR /app

# 複製套件描述檔
COPY package.json package-lock.json ./

# 安裝套件
RUN npm install

# 複製所有原始碼
COPY . .

# 建置應用
RUN npm run build

# 暴露埠號
EXPOSE 3000

# 啟動應用
CMD ["npm", "start"]

這個 Dockerfile 看似沒問題,但其實存在嚴重的效能與安全性問題。讓我們一一來看:

問題 1:使用完整的 node:20 作為基底 node:20 的體積約為 1.1 GB。但我們的應用在執行階段其實不需要 npm、編譯工具或系統套件。我們應該使用更輕量的 node:20-alpine(約 120 MB)或使用多階段建置。

問題 2:先 COPY 原始碼再安裝套件 這會導致 Docker 的快取機制失效。每當你修改了任何原始碼,COPY . . 這層的快取就會失效,迫使 Docker 重新執行 npm install。正確做法是先複製描述檔,安裝套件,再複製原始碼。

問題 3:使用 root 使用者執行 預設情況下,容器會以 root 身份執行。這違反了安全最佳實務(最小權限原則),如果攻擊者突破了應用,就可以直接控制容器。

第二步:優化版本(中階 Dockerfile)

FROM node:20-alpine

WORKDIR /app

# 先複製 package.json,利用 Docker 快取機制
COPY package.json package-lock.json ./

# 安裝正式環境套件(不含 devDependencies)
RUN npm ci --only=production

# 複製原始碼(這層會因為原始碼變動而失效,但 npm install 已快取)
COPY . .

# 建置
RUN npm run build

# 使用非 root 使用者
USER node

EXPOSE 3000

CMD ["npm", "start"]

這個版本已經解決了快取與安全性問題,但 Image 體積仍然偏大(約 400 MB),因為最終 Image 包含了建置工具與原始碼。

第三步:多階段建置(進階 Dockerfile)

多階段建置 (Multi-stage Build) 是 Docker 最強大的最佳化技巧之一。它的概念是:

  1. 第一階段 (Builder):使用完整的 Image,安裝全部工具,進行編譯與建置
  2. 第二階段 (Runner):使用最精簡的 Image,只從第一階段複製建置成果

最終的 Image 只包含「運行所需的最小檔案」,完全不包含編譯器、npm 套件或原始碼。

# === 第一階段:建置階段 ===
FROM node:20-alpine AS builder

WORKDIR /app

# 複製描述檔並安裝全部套件(包含 devDependencies)
COPY package.json package-lock.json ./
RUN npm ci

# 複製原始碼並建置
COPY . .
RUN npm run build

# === 第二階段:執行階段 ===
FROM node:20-alpine AS runner

WORKDIR /app

# 設定正式環境
ENV NODE_ENV=production

# 只複製必要的生產依賴
COPY package.json package-lock.json ./
RUN npm ci --only=production

# 從 builder 階段複製建置成果
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
COPY --from=builder /app/next.config.js ./

# 使用非 root 使用者
USER node

EXPOSE 3000

# 健康檢查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1

CMD ["npm", "start"]

多階段建置的優勢:

  • Image 體積大幅縮小:從 1.1 GB → 約 150 MB
  • 安全性提升:執行階段不包含原始碼或建置工具
  • 部署速度加快:Image 越小,上傳下載越快
  • 快取效率最大化:每個階段獨立快取

建置 Docker Image

Dockerfile 寫好之後,我們可以用 docker build 指令來建立 Image:

# 在專案根目錄執行(Dockerfile 所在的目錄)
docker build -t my-next-app:latest .

指令解析

  • docker build:建置 Image
  • -t my-next-app:latest:指定 Image 的名稱 (tag),格式為 名稱:版本
  • .:指定 Dockerfile 所在的上下文路徑 (Build Context)

建置過程中,你會看到 Docker 一步一步地執行 Dockerfile 中的指令。第一次建置可能需要數分鐘,因為需要下載基底 Image 並安裝套件。

驗證 Image

建置完成後,用以下指令查看所有本機上的 Image:

docker images

輸出範例:

REPOSITORY      TAG       IMAGE ID       CREATED          SIZE
my-next-app     latest    a1b2c3d4e5f6   2 minutes ago    152MB
node            20-alpine 123abc456def   2 weeks ago      124MB

啟動容器

Image 建立好之後,就可以用 docker run 來啟動容器:

# 啟動容器,將主機的 3000 埠映射到容器的 3000 埠
docker run -d -p 3000:3000 --name my-app my-next-app:latest

指令解析

  • -d:背景執行 (detached)
  • -p 3000:3000:連接埠映射 (主機埠:容器埠)
  • --name my-app:指定容器名稱
  • my-next-app:latest:使用的 Image

啟動後,開啟瀏覽器連到 http://localhost:3000,你應該可以看到應用正在運行!

[!TIP] 如果你啟動容器後發現無法連線,請確認你的應用監聽的埠號是否與 EXPOSE 相符。如果應用預設監聽 3000 但 Dockerfile 寫 EXPOSE 8080,就會出現埠號不一致的問題。

實用容器管理指令

# 查看運行中的容器
docker ps

# 查看所有容器(包含已停止的)
docker ps -a

# 查看容器日誌
docker logs my-app

# 即時追蹤日誌
docker logs -f my-app

# 進入容器內部(除錯用)
docker exec -it my-app sh

# 停止容器
docker stop my-app

# 啟動已停止的容器
docker start my-app

# 刪除容器
docker rm my-app

本日總結

在本章中,你學到了:

  1. Dockerfile 基本指令:FROM、WORKDIR、COPY、RUN、CMD 等核心語法
  2. 三種 Dockerfile 寫法:從基礎版到多階段建置的進化過程
  3. 快取機制:如何安排指令順序以最大化 Docker 快取效益
  4. 多階段建置:將 Image 體積從 1.1 GB 縮小到 150 MB
  5. Image 建置與容器啟動:使用 docker build 與 docker run
  6. 容器管理:查看日誌、進入容器、停止與刪除

下一章,我們將學習 Docker Compose——一次啟動多個相互關聯的容器!

會員專屬免費教學

本章節為註冊會員專屬的免費開放內容!請先登入或註冊會員,即可立即解鎖閱讀。

立即登入 / 註冊