使用 Docker Compose 構建全棧應用:React、Express 與 MySQL

作者: Calpa Liu
字數:3380
出版:2025年3月23日
分類: JavaScript Node.js TypeScript 後端開發 Docker Docker ComposeReactExpressMySQL Vite 全棧開發容器化

在複雜的全棧開發環境中,管理前端、後端和數據庫之間的配置和依賴關係可能會非常耗時且易於出錯。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 會管理這個卷,即使容器被移除也會保留數據

關鍵配置詳解

  1. 容器編排策略

    • depends_on: 確保服務的啟動順序,但只能保證容器啟動,不能保證服務已完全就緒
    • restart: 定義容器的重啟策略,unless-stopped 確保服務持續運行
    • healthcheck: 為數據庫增加健康檢查,確保它已完全初始化
  2. 數據持久化

    • 命名卷 (mysql_data): 即使容器被移除,也能保留數據
    • 掛載 init.sql: 容器首次啟動時自動執行 SQL 腳本,初始化數據庫結構和數據
  3. 開發環境最佳實踐

    • 容器命名:使用 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
Express

接下來,設置 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 初始化

MySQL
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);

這個初始化腳本會:

  1. 創建必要的數據庫表
  2. 插入測試數據
  3. 設置適當的索引以提高性能

注意:初始化腳本只會在 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

啟動後,你可以訪問:

你應該能看到前端頁面顯示來自後端的數據,後端則從 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 都能幫助你簡化開發流程、減少環境差異帶來的問題,並最終提高應用質量和開發效率。

感謝您閱讀我的文章。歡迎隨時分享你的想法。
JavaScript Node.js TypeScript 後端開發 Docker Docker ComposeReactExpressMySQL Vite 全棧開發容器化
關於 Calpa

Calpa 擅長使用 TypeScriptReact.jsVue.js 建立 Responsive Website。

他積極參與開源社區,曾在 2019 年的香港開源大會上擔任講者,提供工作經驗和見解。此外,他也在 GitHub 上公開分享個人博客程式碼,已獲得超過 300 顆星星和 60 個分支的支持。

他熱愛學習新技術,並樂意分享經驗。他相信,唯有不斷學習才能跟上快速演變的技術環境。