在複雜的全棧開發環境中,管理前端、後端和數據庫之間的配置和依賴關係可能會非常耗時且易於出錯。Docker Compose 提供了一個優雅的解決方案,使開發人員能夠輕鬆地定義、配置和運行多容器應用程序。本文將詳細介紹如何使用 Docker Compose 創建一個整合了 React.js 前端、Express.js 後端和 MySQL 數據庫的全棧應用,並提供一個使用 Vite、Express 和 MySQL 的實用示例。
為什麼選擇 Docker Compose 進行全棧開發?
在現代全棧開發中,我們通常需要同時運行多個服務:前端應用、後端 API、數據庫、緩存服務等。傳統上,開發人員需要在本地機器上分別啟動這些服務,這不僅繁瑣,還可能導致「在我機器上能運行」的問題。
Docker Compose 通過提供一個聲明式的配置文件,讓我們能夠定義、配置和協調多個 Docker 容器,從而解決這些挑戰。它是 Docker 生態系統中的關鍵工具,特別適合開發和測試環境。
Docker Compose 的核心優勢
Docker Compose 為全棧開發帶來了以下顯著優勢:
-
簡化的服務編排:使用單一命令
docker-compose up
啟動整個應用程序的所有服務,無需手動管理多個終端窗口 -
隔離的網絡環境:每個 Docker Compose 項目都有自己的隔離網絡,避免不同項目之間的端口衝突
-
服務發現與互聯:容器可以通過服務名稱相互訪問,例如後端可以通過
db
主機名連接到數據庫,而不是localhost
-
環境變量管理:可以在
docker-compose.yml
中定義環境變量,或通過.env
文件集中管理,確保配置的一致性 -
開發/生產環境一致性:通過使用相同的容器配置,大大減少了「在我機器上能運行」的問題
-
獨立的數據庫環境:每個開發者可以擁有自己的數據庫實例,避免共享開發數據庫時的衝突
-
快速入職新團隊成員:新開發者只需克隆代碼庫並運行
docker-compose up
,即可獲得完整的開發環境,無需繁瑣的環境配置 -
版本控制的環境配置:環境配置可以與代碼一起版本控制,確保所有開發者使用相同的依賴版本
項目結構設置
在開始之前,我們需要設置適當的項目結構:
docker-fullstack/
├── frontend/ # React 前端(使用 Vite)
│ ├── Dockerfile
│ └── ...
├── backend/ # Express 後端
│ ├── Dockerfile
│ └── ...
├── init.sql # MySQL 初始化腳本
├── .dockerignore
├── docker-compose.yml
└── README.md
創建 Dockerfile 文件
前端 Dockerfile (Vite + React)
# 使用輕量級 Node.js 映像作為基礎
# 選擇 Alpine 版本可大幅減少映像大小
FROM node:22-alpine
# 設置工作目錄
WORKDIR /app
# 先複製 package.json 和 package-lock.json
# 這樣可以利用 Docker 的層級快取,加快後續構建
COPY package*.json ./
# 安裝依賴包
RUN npm install
# 複製其餘源代碼
COPY . .
# 開放 Vite 開發服務器的默認端口
EXPOSE 5173
# 啟動 Vite 開發服務器
# --host 0.0.0.0 參數允許容器外部訪問
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
後端 Dockerfile (Express.js)
# 使用相同的 Node.js 基礎映像確保一致性
FROM node:22-alpine
# 設置工作目錄
WORKDIR /app
# 先複製 package 文件以利用快取
COPY package*.json ./
# 安裝依賴包
# 在生產環境中應考慮使用 npm ci 以確保精確的依賴版本
RUN npm install
# 複製後端源代碼
COPY . .
# 開放 Express 服務器端口
EXPOSE 3000
# 啟動後端應用
# 使用 nodemon 在開發環境中實現熱重載
CMD ["npm", "run", "dev"]
.dockerignore 文件
創建 .dockerignore
文件以排除不必要的文件,減少構建上下文大小並提高安全性:
node_modules
.git
.gitignore
.env
*.log
dist
build
.DS_Store
編寫 docker-compose.yml
Docker Compose 的核心是 docker-compose.yml
文件,它使用聲明式語法定義所有容器及其關係。以下是我們的完整配置,它定義了三個主要服務:
services:
# 前端服務配置
frontend:
build: ./frontend # 使用 ./frontend 目錄中的 Dockerfile 構建映像
container_name: react-frontend # 指定容器名稱,方便識別
ports:
- "5173:5173" # 將容器的 5173 端口映射到主機的 5173 端口
volumes:
- ./frontend:/app # 掛載本地前端代碼到容器中,支持即時更新
- /app/node_modules # 將容器內的 node_modules 排除在掛載外,避免被本地覆蓋
depends_on:
- backend # 確保後端服務先啟動
environment:
- VITE_API_URL=http://localhost:3000 # 設置 API 的基礎 URL
restart: unless-stopped # 除非手動停止,否則繼續重啟
# 後端服務配置
backend:
build: ./backend # 使用 ./backend 目錄中的 Dockerfile 構建映像
container_name: express-backend # 指定容器名稱
ports:
- "3000:3000" # 將容器的 3000 端口映射到主機的 3000 端口
volumes:
- ./backend:/app # 掛載本地後端代碼到容器中,支持即時更新
- /app/node_modules # 保護容器內的 node_modules
depends_on:
- db # 確保數據庫服務先啟動
environment:
- NODE_ENV=development # 設置環境變量
- DB_HOST=db # 注意這裡使用服務名稱作為主機名,而非 localhost
- DB_USER=root
- DB_PASSWORD=password # 在生產環境中應使用更安全的密碼
- DB_NAME=myapp
- DB_PORT=3306
- LANG=C.UTF-8
restart: unless-stopped # 自動重啟策略
# 數據庫服務配置
db:
image: mysql:8.0 # 使用官方 MySQL 8.0 映像
container_name: mysql-db # 指定容器名稱
ports:
- "3306:3306" # 將 MySQL 端口映射到主機
environment:
- MYSQL_ROOT_PASSWORD=password # 設置 root 密碼
- MYSQL_DATABASE=myapp # 自動創建的數據庫名稱
- MYSQL_USER=user # 可選創建一個非 root 用戶
- MYSQL_PASSWORD=userpassword # 非 root 用戶的密碼
volumes:
- mysql_data:/var/lib/mysql # 持久化數據存儲
- ./init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化腳本,容器首次啟動時執行
restart: unless-stopped # 確保數據庫服務繼續運行
healthcheck: # 健康檢查確保數據庫已完全啟動
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
# 定義命名卷以持久化數據
volumes:
mysql_data: # Docker 會管理這個卷,即使容器被移除也會保留數據
關鍵配置詳解
-
容器編排策略
depends_on
: 確保服務的啟動順序,但只能保證容器啟動,不能保證服務已完全就緒restart
: 定義容器的重啟策略,unless-stopped
確保服務持續運行healthcheck
: 為數據庫增加健康檢查,確保它已完全初始化
-
數據持久化
- 命名卷 (
mysql_data
): 即使容器被移除,也能保留數據 - 掛載
init.sql
: 容器首次啟動時自動執行 SQL 腳本,初始化數據庫結構和數據
- 命名卷 (
-
開發環境最佳實踐
- 容器命名:使用
container_name
為容器指定易識別的名稱,方便調試 - 代碼熱重載:通過掛載本地目錄到容器中,修改本地代碼會自動反映到容器中
- 保護 node_modules: 使用
/app/node_modules
卷確保容器內的依賴不被本地覆蓋
- 容器命名:使用
Hello World 示例
現在讓我們創建一個簡單的 Hello World 示例,展示如何使用 Vite、Express 和 MySQL 返回簡單數據。
1. 前端設置 (Vite + React)
首先,創建 React 前端應用:
mkdir -p docker-fullstack/frontend
cd docker-fullstack/frontend
npm create vite@latest . -- --template react
npm install axios
修改 frontend/src/App.jsx
:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [message, setMessage] = useState('')
const [loading, setLoading] = useState(true)
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('http://localhost:3000/api/hello')
const data = await response.json()
setMessage(data.message)
setLoading(false)
} catch (error) {
console.error('Error fetching data:', error)
setLoading(false)
}
}
fetchData()
}, [])
return (
<div className="App">
<h1>Docker Compose 全棧 Demo</h1>
{loading ? (
<p>正在加載後端數據...</p>
) : (
<div>
<h2>來自後端的訊息:</h2>
<p>{message}</p>
</div>
)}
</div>
)
}
export default App
2. 後端設置 (Express.js)

接下來,設置 Express 後端:
mkdir -p docker-fullstack/backend
cd docker-fullstack/backend
npm init -y
npm install express cors mysql2 nodemon
創建 backend/index.js
:
const express = require('express');
const cors = require('cors');
const mysql = require('mysql2/promise');
const app = express();
const port = process.env.PORT || 3000;
// 中間件
app.use(cors());
app.use(express.json());
// 數據庫連接配置
const dbConfig = {
host: process.env.DB_HOST || 'localhost',
user: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'myapp',
port: process.env.DB_PORT || 3306,
};
app.get('/', (req, res) => {
res.send('Hello World!');
});
// Hello World 路由
app.get('/api/hello', async (req, res) => {
try {
// 創建連接
const connection = await mysql.createConnection(dbConfig);
// 查詢數據庫獲取訊息
const [rows] = await connection.execute('SELECT * FROM messages LIMIT 1');
// 關閉連接
await connection.end();
if (rows.length > 0) {
res.json({ message: rows[0].content });
} else {
res.json({ message: '數據庫中未找到訊息' });
}
} catch (error) {
console.error('數據庫錯誤:', error);
res.status(500).json({ message: 'Express 問候!(數據庫連接失敗)' });
}
});
// 啟動服務器
app.listen(port, () => {
console.log(`服務器運行在 ${port} 端口`);
});
修改 backend/package.json
添加開發腳本:
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
3. MySQL 初始化

在 Docker Compose 環境中,我們可以使用初始化腳本自動設置數據庫。MySQL 官方映像會在容器首次啟動時執行 /docker-entrypoint-initdb.d/
目錄中的所有 SQL 腳本。
在項目根目錄創建 init.sql
:
-- 創建我們的消息表
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
content VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
COMMENT VARCHAR(255)
);
-- 設定數據庫的字符集和排序
ALTER DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 插入測試數據
INSERT INTO messages (content, COMMENT) VALUES
('來自 MySQL 數據庫的問候!', '初始消息'),
('Docker Compose 讓全棧開發變得簡單!', '第二條消息');
-- 創建一個用戶表作為示例
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 添加索引以提高查詢效率
CREATE INDEX idx_messages_created_at ON messages(created_at);
CREATE INDEX idx_users_username ON users(username);
這個初始化腳本會:
- 創建必要的數據庫表
- 插入測試數據
- 設置適當的索引以提高性能
注意:初始化腳本只會在 MySQL 容器首次啟動時執行。如果你需要重新執行腳本,可以刪除 MySQL 數據卷:
docker compose down -v # 刪除所有卷
# 或者只刪除 MySQL 數據卷
docker volume rm docker-fullstack_mysql_data
4. 運行項目
在項目根目錄執行以下命令來啟動所有服務:
# 在後台啟動所有服務
docker compose up -d
# 查看容器日誌
docker compose logs -f
# 查看特定服務的日誌
docker compose logs -f backend
5. 常用 Docker Compose 命令
# 停止所有容器
docker compose down
# 停止並刪除卷
docker compose down -v
# 重啟特定服務
docker compose restart backend
# 查看容器狀態
docker compose ps
# 執行容器內的命令
docker compose exec backend sh
啟動後,你可以訪問:
- 前端應用:http://localhost:5173
- 後端 API:http://localhost:3000/api/hello
你應該能看到前端頁面顯示來自後端的數據,後端則從 MySQL 數據庫中讀取數據。
Docker Compose 最佳實踐
1. 使用多階段構建減少映像大小
在生產環境中,應使用多階段構建來減少最終映像的大小:
# 構建階段
FROM node:22-alpine as build
WORKDIR /app
COPY package*.json ./
# 使用 npm ci 確保精確安裝 package-lock.json 中的依賴
RUN npm ci
COPY . .
# 創建生產構建
RUN npm run build
# 生產階段
FROM nginx:alpine
# 只複製構建階段的輸出文件
COPY --from=build /app/dist /usr/share/nginx/html
# 複製自定義 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
2. 使用 .env 文件管理環境變量
創建 .env
文件存儲環境變量,然後在 docker-compose.yml 中引用:
# docker-compose.yml
services:
backend:
env_file: .env
創建 .env.example
文件作為範本,不包含敏感信息:
# .env.example
DB_HOST=db
DB_USER=root
DB_PASSWORD=example_password
DB_NAME=myapp
3. 安全考慮
- 避免在映像中存儲敏感信息:使用環境變量或安全的密鑰管理工具
- 定期更新基礎映像:確保使用最新的安全補丁
- 限制容器權限:避免使用 root 用戶運行容器
# 在 Dockerfile 中添加非 root 用戶
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
4. 使用 Docker Compose 覆蓋文件
為不同環境創建多個配置文件:
# 基礎配置
docker-compose.yml
# 開發環境特定配置
docker-compose.override.yml
# 生產環境配置
docker-compose.prod.yml
使用特定環境的配置:
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
2. 選擇合適的基礎映像
選擇最小的、有安全保障的基礎映像,如官方認證的映像或 Alpine 版本:
FROM node:22-alpine
而不是:
FROM node:22
3. 創建短暫的容器
容器應該是短暫的,可以停止、銷毀、重建和替換,只需最少的設置和配置。
4. 使用 .dockerignore 排除不必要的文件
創建 .dockerignore
文件以排除不必要的文件,減少構建上下文大小:
node_modules/
npm-debug.log
build/
.git/
.env
5. 分離應用程序職責
每個容器應該只有一個職責,這樣更容易水平擴展和重用容器。例如,將前端、後端和數據庫分別放在不同的容器中。
6. 不要安裝不必要的包
避免在容器中安裝不必要的包,以減少複雜性、依賴性、文件大小和構建時間。
7. 適當使用環境變量
使用環境變量來配置應用程序,這樣可以在不同環境中靈活運行:
environment:
- NODE_ENV=development
- DB_HOST=db
- DB_USER=root
- DB_PASSWORD=password
8. 持久化重要數據
使用命名卷來持久化重要數據,如數據庫數據:
volumes:
mysql_data:
結論
Docker Compose 為全棧開發提供了一個強大而靈活的環境,大大簡化了配置和管理多容器應用的過程。通過本文介紹的步驟和最佳實踐,開發者可以快速構建一個包含 React、Express 和 MySQL 的完整開發環境,並確保它能夠一致地在任何地方運行。
Docker Compose 不僅提高了開發效率,還促進了團隊協作,使新成員能夠快速熟悉和參與項目開發。通過適當的容器化策略,可以創建一個更加模塊化、可維護和可擴展的應用架構,為現代 Web 開發提供堅實的基礎。
無論你是初學者還是有經驗的開發者,使用 Docker Compose 都能幫助你簡化開發流程、減少環境差異帶來的問題,並最終提高應用質量和開發效率。