【從一台 Server 到分散式架構】第 21 篇:廚房變成標準化套餐——容器化(Docker)的動機
某個週五下午,小明把新版本部署到測試環境,畫面一片白。
他 SSH 進去測試機查,發現 Node.js 版本是 16,而他本機是 18。某個套件的 API 在兩個版本之間有破壞性變更。
改好之後,他把版本對齊,重新部署。這次測試環境通了,但上到正式環境又掛了——正式環境的 Redis 版本跟開發環境不一樣,連接方式有差異。
小傑看著小明來回改了三次,說:
「你在修的不是程式,是環境差異。這種事每次部署都要來一遍,因為你部署的只是程式碼,不是整個執行環境。」
小明說:「那怎麼辦?」
小傑:「把環境也打包進去。」
生活化的比喻:廚師帶著整個廚房出差
傳統部署方式,像是廚師只帶著食譜去陌生的廚房做菜。
他假設那個廚房有他需要的鍋具、調味料、火候。但每個廚房都不一樣——有的爐子火太大、有的沒有特定的香料、有的鍋子形狀不對。
廚師每次都要花時間「適應新廚房」,才能開始做菜。
容器化的做法是:廚師帶著整個廚房出差——連鍋具、調味料、食材一起打包成一個標準化的「套餐」。
不管你把這個套餐搬到哪裡——自己電腦、測試機、正式環境、別的雲端——打開來都是一樣的廚房,廚師可以直接開始做菜。
這就是 Docker 在解決的問題。
核心概念:Image 和 Container
Docker 有兩個最基本的概念:
Image(映像檔)——食譜 + 備好的食材
Image 是一個靜態的、唯讀的打包檔,裡面包含:
- 作業系統層(通常是輕量的 Linux)
- 執行環境(Node.js 18、Python 3.11……)
- 應用程式程式碼
- 套件依賴(
package.json裡的所有東西) - 設定檔
Image 是「可以重現的快照」。你在任何地方用同一個 Image,得到的環境都一模一樣。
Container(容器)——正在執行的廚房
Container 是從 Image 啟動起來的執行中實例。
一個 Image 可以同時啟動多個 Container,每個 Container 彼此隔離——就像同一份食譜,同時在三個廚房各做一份。
類比整理:
| 概念 | 比喻 |
|---|---|
| Dockerfile | 食譜(描述怎麼建造環境) |
| Image | 照著食譜備好的標準化廚房(靜態) |
| Container | 正在運作的廚房(動態) |
Dockerfile:告訴 Docker 怎麼打包
你需要寫一個 Dockerfile 來描述 Image 應該長什麼樣:
# 從官方 Node.js 18 基底開始
FROM node:18-alpine
# 設定工作目錄
WORKDIR /app
# 先複製 package.json,安裝依賴(善用快取)
COPY package*.json ./
RUN npm ci --only=production
# 再複製應用程式程式碼
COPY . .
# 開放 3000 port
EXPOSE 3000
# 啟動指令
CMD ["node", "server.js"]執行 docker build -t course-api:v1.0 .,Docker 就會照著這份 Dockerfile 建出一個 Image。
容器化帶來的不只是「到處能跑」
環境一致性只是 Docker 的第一個好處,它還帶來幾個連鎖效應:
1) 部署流程標準化
沒有容器化之前,部署是這樣:
SSH 進機器 → 拉 code → 跑 npm install → 重啟 PM2 → 祈禱
容器化之後,部署變成:
推新的 Image → 啟動新 Container → 舊 Container 下線
每個環境的部署步驟一樣,不再依賴機器上的「歷史狀態」。
2) 快速橫向擴充
要多跑幾個 API 服務的副本,只需要多起幾個 Container:
docker run -d course-api:v1.0
docker run -d course-api:v1.0
docker run -d course-api:v1.0三台副本,三秒內啟動完畢。
3) 隔離性——一台機器跑多個服務不互相干擾
以前一台機器上同時跑訂單服務和支付服務,如果它們依賴不同版本的 Python,就會衝突。
容器化後,每個服務都住在自己的容器裡,各自有自己的執行環境,彼此不干擾。
4) 開發環境一致
新人加入團隊,以前要花半天設定開發環境;現在:
docker-compose up一行指令,把整個開發環境(API + DB + Redis)全部啟動起來,跟其他人的環境一模一樣。
容器不是虛擬機
很多人第一次聽到容器,會以為它就是虛擬機(VM)的縮小版。兩者有本質差異:
| 虛擬機(VM) | 容器(Container) | |
|---|---|---|
| 隔離層級 | 硬體層(每個 VM 有自己的 OS kernel) | 作業系統層(共用 host kernel) |
| 啟動時間 | 分鐘級 | 秒級(甚至毫秒) |
| 佔用空間 | GB 級 | MB 級 |
| 隔離強度 | 強 | 較弱(共用 kernel,有逃逸風險) |
容器比 VM 輕量很多,啟動更快,適合作為服務部署單位。需要強隔離的場景(例如給不同客戶跑不同程式碼),還是需要 VM。
小明的落地做法
第一步:給每個服務寫 Dockerfile
每個服務(訂單、支付、通知…)各自有一個 Dockerfile
第二步:本地開發用 docker-compose
一個 docker-compose.yml 定義整個開發環境:
API + PostgreSQL + Redis + mock 外部服務
第三步:CI/CD 推 Image
每次 push 到主分支,CI 自動建 Image 推到 Image Registry
(Docker Hub / AWS ECR / Google GCR)
第四步:正式環境拉 Image 跑
正式機器從 Registry 拉指定版本的 Image,啟動 Container
小結與預告
這篇小明學到的重點是:
- 容器化解決環境不一致:把程式碼和執行環境一起打包成 Image,任何地方跑都一樣。
- Image vs Container:Image 是靜態快照(食譜),Container 是執行中的實例(廚房)。
- 容器不只是「能跑」:還帶來部署標準化、快速擴充、服務隔離、開發環境一致。
- 容器比 VM 輕量:秒級啟動、MB 級大小,適合服務部署。
容器解決了「在哪裡跑都一樣」的問題。但當你有幾十個、幾百個容器分散在幾十台機器上——誰來決定每個容器要跑在哪台機器?容器掛掉了誰來重啟它?流量多了誰來自動加容器?
下一篇,我們來談負責「管理一群容器」的系統——Kubernetes。