前面一路做到現在,Node.js、Express、PostgreSQL、JWT、Transaction 都已經慢慢串起來了。
但當專案開始變得比較完整之後,很容易遇到一個經典問題:「我的電腦明明可以跑,為什麼你那邊不行?」
可能是 Node.js 版本不同,也可能是套件版本、環境變數、作業系統或其他設定不同,最後都可能讓同一份程式出現不同結果。這些都可以算是程式執行時所依賴的環境。
Docker 想處理的事情很簡單:把程式需要的執行環境整理起來,讓同一份程式可以在接近一致的環境裡執行。
在開始使用 Docker 之前,電腦需要先安裝 Docker Desktop。
Docker Desktop 可以讓我們直接在自己的電腦上使用 Docker。安裝完成後,先打開 Docker Desktop,確認 Docker 已經正常啟動,再到 Terminal 執行:
docker --version
如果有看到 Docker 的版本資訊,就代表已經可以開始使用。
可以先把 Container 想成一個獨立的執行環境,程式會在裡面運作,需要的 Node.js、套件和專案檔案也可以一起準備好。
這樣一來,程式執行時對自己電腦環境的依賴就會少很多。今天的目標很簡單,就是把原本直接在電腦上執行的 Express API,改成在 Docker Container 裡執行。
第一次接觸 Docker,很容易把 Image 和 Container 搞混。可以先把 Image 想成一份「環境範本」,裡面定義了程式執行時需要的內容;Container 則是使用這份 Image 建立出來的實際執行實例。
例如我們準備好一個 Node.js Image,裡面包含 Node.js、套件和程式需要的檔案,接著透過 Docker 啟動它,就會建立出一個 Container,Express API 也會在這個 Container 裡運作。
兩者的關係可以先記成:
Image
↓
Container
同一個 Image 可以建立多個 Container,所以可以先記住:Image 是準備好的環境範本,Container 是實際執行的結果。
既然 Image 是環境範本,那這份範本要怎麼建立?
這就會用到 Dockerfile。
Dockerfile 是一個文字檔,裡面寫著建立 Image 時需要準備什麼環境、安裝哪些套件、放入哪些檔案,以及啟動程式時要執行什麼指令。
假設現在有一個 Express API,專案結構大概是:
my-api/
├── node_modules/
├── src/
│ └── server.js
├── package.json
├── package-lock.json
├── .dockerignore
└── Dockerfile
Dockerfile 可以寫成:
FROM node:24-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
這份 Dockerfile 可以從上到下看,FROM 先準備 Node.js 24 的基礎環境,WORKDIR 設定 Container 裡的工作目錄,接著把 package.json 和 package-lock.json 複製進去,再透過 npm ci 安裝專案需要的套件,最後把其他專案檔案複製進 Container。EXPOSE 3000 宣告應用程式預計使用 3000 Port,CMD 則指定 Container 啟動後要執行的指令,也就是 npm start。
先不用急著把每個指令背起來,現在只要知道 Dockerfile 的工作,就是把「程式需要什麼環境」寫下來,之後 Docker 才能根據這份設定建立 Image。
.dockerignore:哪些檔案不要一起帶進去?在建立 Image 之前,可以先準備 .dockerignore:
node_modules
npm-debug.log
.git
.env
這個檔案可以想成 Git 的 .gitignore。.gitignore 是告訴 Git 哪些檔案不要放進版本控制;.dockerignore 則是告訴 Docker 哪些檔案不要送進 Build Context。
例如 node_modules 會在 Container 裡透過 npm ci 重新安裝,就不需要把自己電腦上的版本一起複製進去;.env 也應該排除,避免資料庫密碼、JWT Secret 等敏感資訊被一起放進 Image。
接下來把 Express API 放進 Docker。
先確認 package.json 裡面有啟動指令:
{
"scripts": {
"start": "node src/server.js"
}
}
Express Server 也要注意監聽的位址。如果原本寫的是:
app.listen(3000, 'localhost', () => {
console.log('Server is running on port 3000');
});
放進 Container 後,外面的電腦可能連不到這個 Server,因為 localhost 指的是 Container 自己,所以這裡改成監聽 0.0.0.0:
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/hello', (req, res) => {
res.json({
message: 'Hello Docker'
});
});
app.listen(3000, '0.0.0.0', () => {
console.log('Server is running on port 3000');
});
這樣 Express 就會監聽 Container 對外可使用的網路介面。
今天先把 Express API 放進 Container,PostgreSQL 暫時維持原本的環境,等之後學到 Docker Compose,再把 API、資料庫等多個服務一起管理。
準備好 Dockerfile 和 .dockerignore 之後,就可以建立 Image。
在專案根目錄執行:
docker build -t my-api .
這個指令會根據 Dockerfile 建立 Image。-t my-api 是替 Image 取名字,這裡取名為 my-api;最後的 . 則代表目前這個資料夾,也就是 Docker 建立 Image 時使用的 Build Context。
建立完成後,就有了一份可以拿來建立 Container 的 Image。
有了 Image 之後,接著執行:
docker run --name my-api-container -p 3000:3000 my-api
這個指令會使用 my-api Image 建立並啟動 Container。--name my-api-container 用來替 Container 取名字,-p 3000:3000 則是把自己電腦的 3000 Port 連到 Container 裡的 3000 Port,最後的 my-api 就是這次要使用的 Image。
所以整行可以理解成:
使用
my-api這個 Image,建立一個叫做my-api-container的 Container,並把電腦的 3000 Port 連到 Container 的 3000 Port。
這時候在瀏覽器輸入:
http://localhost:3000/api/hello
就可以看到 Express API 的回應。
這裡也可以順便理解 EXPOSE 和 -p 的差別:Dockerfile 裡的 EXPOSE 3000 是宣告應用程式預計使用的 Port;docker run 裡的 -p 3000:3000 才是把自己電腦和 Container 的 Port 實際連接起來。
第一次執行 docker run 時,Terminal 會持續顯示 Express Server 的輸出,這代表 Container 正在前景執行。先不要關掉這個 Terminal,再開瀏覽器測試 API。
Day 24 先把最重要的兩個 Docker 指令跑熟:
docker build -t my-api .
建立 Image。
docker run --name my-api-container -p 3000:3000 my-api
使用 Image 建立並啟動 Container。
之後再慢慢補上其他常用指令:
docker ps:查看正在執行的 Containerdocker logs:查看 Container 的 Logdocker stop:停止 Containerdocker start:重新啟動 Containerdocker images:查看目前的 ImageDocker 還有一些更進階的功能,例如 Docker Compose、Volume、Network、Environment Variables 等,先不用一次全部記住。
現在回頭看一開始的問題:「我的電腦可以跑,為什麼你的不行?」
Node.js、套件和其他執行環境,都可能造成差異。Docker 讓我們可以把這些需求寫進 Dockerfile,再建立成 Image,最後讓程式在 Container 裡執行。
今天先把這條流程跑起來:
Dockerfile
↓
docker build
↓
Image
↓
docker run
↓
Container
這就是今天 Docker 入門最重要的一條路。
今天先從 Image 和 Container 開始,理解 Docker 怎麼把程式需要的環境整理起來。
接著用 Dockerfile 建立 Image,再把 Express API 放進 Container 跑起來。先把這條流程跑熟,之後再慢慢往更完整的 Docker 使用方式延伸。