假設你寫好一支 Node.js 程式,在自己的電腦執行沒問題。交給同事後,他卻說跑不起來
你用 Node.js 24,他可能還在用舊版。你電腦裡裝好的套件,他那邊也不一定有。明明傳的是同一份程式,怎麼還要花時間對環境?
這就接到今天的 Docker Image(容器映像檔)。我們把程式、套件和需要的執行環境一起打包,交給另一台主機的 Docker 啟動,減少每台電腦各裝各的差異
Image 是打包好的內容,還沒有在替使用者處理請求。用這份 Image 啟動後,正在執行的那一份,才叫 Container(容器)
看下面這張圖,同一份 Image 可以啟動兩個 Container。假設你把其中一個停掉,另一個仍能繼續跑,原本的 Image 也還在
所以看到 Image 列表裡有 demo-api,只能說這份程式已經打包好;要找正在跑的那一份,得看 Container
這也解釋了為什麼部署時會分成 build 和 run:前者是打包,後者才是把程式跑起來。Image 不是正在運作的網站,跟把 ZIP 上傳到硬碟一樣,放好了不代表程式自己會執行,也附上(Docker 的 Image 與 Container 的差異)
如果你平常用 npm run build 打包前端,看到這裡可能會想:現在要改用 Docker,原本那個指令還要跑嗎?
先看 package.json 裡的 scripts.build 寫了什麼。npm run build 會執行你設定的指令;例如 Vite 專案常見的是 vite build,把前端程式建置成 dist 裡的檔案(也附上 npm run 的用途)
docker build 則是照 Dockerfile,把要交給主機的檔案與執行環境做成 Image。所以原本的專案需要編譯,這一步仍然要做,也可以安排在 Dockerfile 裡執行 RUN npm run build
習慣用 npm 管理指令也可以。假設你把 docker build -t demo-api:v1 . 設成 scripts 裡的 docker:build,就能用 npm run docker:build 呼叫它。實際做的事相同,只是入口換成你熟悉的 npm
這次的小程式直接由 Node.js 執行,我們先用 Docker 指令看清楚整個流程
我們先不放完整網站,只做一個能回應 HTTP 的 Node.js 範例。電腦要先有 Docker,並確認 docker version 看得到 Client 和 Server;只有指令裝好了,Docker 引擎沒啟動,一樣不能執行容器
Windows 可以安裝 Docker Desktop(Windows),依官方要求準備 WSL 2,使用 Linux containers;Mac 則依 Intel 或 Apple Silicon 選 對應安裝版
這篇都在自己的電腦操作,還不用 SSH 到 EC2。在放練習專案的位置執行:
mkdir demo-api
cd demo-api
用 VS Code 開啟這個資料夾,新增 server.cjs:
const http = require('node:http');
http.createServer((req, res) => {
const ok = req.method === 'GET' && req.url === '/health';
res.writeHead(ok ? 200 : 404, {
'Content-Type': 'application/json; charset=utf-8',
});
res.end(JSON.stringify(ok ? { status: 'ok' } : { error: 'not found' }));
}).listen(8080, '0.0.0.0', () => console.log('API listening on 8080'));
/health 是我們自己寫的路徑,回應 ok 表示這支程式有在處理請求。Docker 不會自動替每個網站加上這個功能
接著在 server.cjs 旁邊新增 Dockerfile,檔名就叫這個,不要加 .txt。它是給 Docker 看的打包步驟,內容如下:
FROM node:24-alpine
WORKDIR /app
COPY server.cjs ./
USER node
EXPOSE 8080
CMD ["node", "server.cjs"]
第一行從已經有 Node.js 的基底 Image 開始,接著設定工作目錄、複製程式,最後指定啟動指令。這支程式只用 Node.js 內建功能,所以不需要 npm install
USER node 是讓應用程式用 Image 裡的 node 帳號執行,不必為了回應 HTTP,給程式容器內的 root 身分
同個資料夾再加 .dockerignore:
.env
.env.*
*.pem
.git
node_modules
這是在限制哪些檔案不要送進打包範圍。尤其改成 COPY . . 時,沒排除 .env,很容易連資料庫密碼一起包進去。就算後來在 Dockerfile 刪掉,也不能當作前面那一層的機密已經消失
現在 demo-api 裡應有三個檔案:server.cjs、Dockerfile、.dockerignore。它們放同一層,再回終端機執行下面三步:先打包、啟動,最後送一次測試請求
每一步沒有出錯,再執行下一步:
docker build -t demo-api:v1 .
docker run --rm -d --name demo-api -p 127.0.0.1:8080:8080 demo-api:v1
curl -i http://127.0.0.1:8080/health
Windows PowerShell 的測試指令請使用 curl.exe -i http://127.0.0.1:8080/health;後面遇到 curl 也改用 curl.exe,避免叫到 PowerShell 的同名指令。其餘指令照貼即可
demo-api 是 Image 名稱,v1 是這一版的標籤。最後的 . 表示以目前資料夾作為打包來源,少了它,Docker 不知道要從哪裡讀檔案
run 裡的 --name demo-api 是替正在跑的容器取名,-d 讓它在背景執行,--rm 表示停止後自動移除這個練習容器。curl 則是用指令發出 HTTP 請求,功能像這次在瀏覽器打開測試網址
這裡有兩個 8080。左邊是自己電腦的 port,右邊是 Container 裡程式監聽的 port;加上 127.0.0.1,表示這次只從自己的電腦連入,不直接開給外面的電腦
把電腦和 Container 畫開,就比較好認了。curl 先找到電腦上的 8080,再由 Docker 對到 Container 的 8080。程式在容器內監聽 0.0.0.0,才收得到從容器外送進來的請求
成功時,HTTP 狀態應是 200,內容是 {"status":"ok"}。再測一次不存在的路徑:
curl -i http://127.0.0.1:8080/missing
這次應回 404 與 {"error":"not found"},不是每個網址都說成功。如果剛剛改用 8081,這裡的網址也要改成 8081
看不到回應時,先用 docker logs demo-api 看程式有沒有啟動。假如剛啟動還沒準備好,等看到 API listening on 8080 後再測一次
之後若修改 server.cjs,目前的 Container 不會自己更新。先儲存檔案,再回到有 Dockerfile 的 demo-api 資料夾,依序執行;每一步成功才繼續:
docker build -t demo-api:v1 .
docker stop demo-api
docker run --rm -d --name demo-api -p 127.0.0.1:8080:8080 demo-api:v1
curl -i http://127.0.0.1:8080/health
中間的 stop 很重要:舊容器還在執行,就占著 demo-api 這個名稱與電腦的 port。先停掉它,啟動時加的 --rm 會自動移除舊容器,新的那份才能使用同樣的名稱
假如你前面已經停止容器、看到 No such container,表示這個名稱已經空出來,可以接著執行 run
記得前面選的是 8081,這裡也沿用 -p 127.0.0.1:8081:8080 與 8081 的測試網址;已啟動後想換 port,同樣先停掉舊容器再重跑 run
重新 build 會更新 demo-api:v1 指向的 Image,但正在跑的舊容器不會跟著換。這也是為什麼只有 build,還看不到程式更新
還有一個條件要對上:CPU 架構,也就是處理器使用的指令系統。x86_64 和 AMD64 在這裡指同一類 64 位元架構,不是只有 AMD 的電腦才能用
例如 Apple Silicon 和 AWS 的 T4g 都是 ARM64,但一般 Intel/AMD 主機常見的是 AMD64。拿錯架構的 Image,可能直接出現 exec format error。Docker 可以處理多平台建置,但不能只因為副檔名或 Image 名稱一樣,就認為裡面的程式一定適合那台機器
我們後續以 ARM64 的 EC2 為例,打包時就明確指定 linux/arm64。所以如果你的 EC2 是 AMD64,整條流程改成 linux/amd64(Docker 多平台建置)
這樣的話用 Windows 或 Mac 都能跟著做,不用為了 EC2 換一台電腦。下面先畫出這次部署到 T4g 的方向,下一篇會真的帶入建置指令
有個細節要提醒下,儘管你程式包好了,不表示資料庫地址、密碼和 AWS 權限也都準備好了
換一個部署環境,這些設定可能不同,這個細節我們後面會在啟動 Container 時另外交給它~
| 動作 | 得到什麼 |
|---|---|
docker build |
一份可以交給主機使用的 Image |
docker run |
一個正在執行的 Container |
呼叫 /health |
確認程式真的有回應 HTTP |
練完執行 docker stop demo-api。剛剛啟動時有加 --rm,所以 Container 停止後會移除,Image 則留著
下一篇我們就來學 ECR,讓 AWS 上的 EC2 也找得到,也下載得下來,我們明天見勒 :D