📋 目錄
上一篇文章介紹了如何把一個 Node.js 服務包成 Docker 容器。但真實應用不會只有一個服務——你還需要資料庫、Redis、新手也不一定會裝的 elasticsearch 或 rabbitmq。Docker Compose 讓你用一個設定檔定義整個開發環境,無論是新進同事還是乾淨的機器,都能「一行指令」啟動完整的開發環境。
前言:為什麼需要 Docker Compose?
多人開發最大的痛苦之一:環境不一致。
A 的機器跑了,B 的機器壞了,C 說「我這裡可以跑啊」。問題常常出在「我的機器有安裝 Redis,你的沒有」、「MySQL 版本不同,SQL 語法有差異」。
Docker Compose 的解決思路:用一個 docker-compose.yml 描述整個開發環境需要的服務,你的機器和我的機器看到的完全一樣。
跟上一篇文章的 docker-compose.yml 相比,這篇要處理的更完整——不只是「一個 API + 一個資料庫」,而是多個基礎設施服務 + Node.js 應用的完整開發棧。
完整開發環境的服務規劃
這次要建立的開發環境包含:
| 服務 | Image | 用途 |
|---|---|---|
app | 自己的 Node.js 專案 | API 應用 |
mysql | mysql:8 | 關聯式資料庫 |
redis | redis:7-alpine | 快取與 session 儲存 |
phpmyadmin | phpmyadmin:latest | 視覺化資料庫管理(可選) |
撰寫 docker-compose.yml
# docker-compose.yml
services:
# Node.js 應用
app:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3000:3000"
volumes:
# 掛載 source code,修改時即時反映(熱重載)
- ./src:/app/src
# 不掛載 node_modules,讓容器內的獨立 node_modules 優先
environment:
- NODE_ENV=development
- DATABASE_URL=mysql://admin:mysecretpassword@db:3306/myapp_dev
- REDIS_URL=redis://cache:6379
depends_on:
db:
condition: service_healthy # 等 db 健康檢查通過後才啟動
cache:
condition: service_started
command: npm run dev # 啟動 development 模式
# MySQL 資料庫
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: rootsecret
MYSQL_DATABASE: myapp_dev
MYSQL_USER: admin
MYSQL_PASSWORD: mysecretpassword
ports:
- "3306:3306"
volumes:
# 資料持久化:主機目錄對應容器內的資料目錄
- mysql_data:/var/lib/mysql
# 初始化 SQL:第一次啟動時自動執行
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Redis 快取
cache:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
# Redis 資料持久化(RDB snapshot)
- redis_data:/data
command: redis-server --appendonly yes # 開啟 AOF 持久化
# phpMyAdmin(可選,視覺化管理 MySQL)
phpmyadmin:
image: phpmyadmin:latest
ports:
- "8080:80"
environment:
PMA_HOST: db
PMA_USER: admin
PMA_PASSWORD: mysecretpassword
depends_on:
- db
profiles:
- tools # 用 --profile tools 才會啟動
volumes:
mysql_data:
redis_data:
初始化資料庫 schema
在 init.sql 放 Prisma 對應的 schema:
-- init.sql
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount INT NOT NULL DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
開發用的 Dockerfile
與生產環境的 Dockerfile 不同,開發用的版本需要保留 source code 掛載能力:
# Dockerfile.dev
FROM node:22-alpine
WORKDIR /app
# 開發模式:保留完整 node_modules,包含 devDependencies
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
# 開發時用 ts-node-dev 熱重載
CMD ["npm", "run", "dev"]
對比一下生產用的 Dockerfile(上一篇文章的多階段建構),開發版不需要多階段建構——因為我們需要 source code 完整存在容器裡以便 volume 掛載。
啟動與管理
# 啟動所有服務(除了 profiles: [tools] 的)
docker compose up -d
# 啟動並即時查看 logs
docker compose up
# 查看所有服務狀態
docker compose ps
# 查看特定服務的 logs
docker compose logs app
docker compose logs db
# 進入 MySQL 容器,直接操作資料庫
docker compose exec db mysql -u admin -pmysecretpassword myapp_dev
# 重啟某個服務
docker compose restart app
# 停止所有服務(但保留 volumes,資料不會消失)
docker compose down
# 停止並刪除 volumes(乾淨清除,資料庫資料會消失)
docker compose down -v
# 重新建構 Image(例如 package.json 改了)
docker compose up --build
# 使用 tools profile(啟動 phpMyAdmin)
docker compose --profile tools up -d
連接 Redis 的實際範例
// src/cache.ts
import { createClient } from 'redis';
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
redisClient.on('error', (err) => console.error('Redis error:', err));
redisClient.on('connect', () => console.log('Redis connected'));
export async function getCachedUser(userId: number) {
const key = `user:${userId}`;
const cached = await redisClient.get(key);
if (cached) {
return JSON.parse(cached);
}
return null;
}
export async function setCachedUser(userId: number, user: object) {
const key = `user:${userId}`;
// 過期時間 5 分鐘
await redisClient.setEx(key, 300, JSON.stringify(user));
}
在 Express route 裡使用:
// src/index.ts
app.get('/api/users/:id', async (req, res) => {
const userId = parseInt(req.params.id);
// 先查 Redis cache
const cached = await getCachedUser(userId);
if (cached) {
return res.json({ source: 'cache', user: cached });
}
// Cache miss,查資料庫
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user) {
return res.status(404).json({ error: '使用者不存在' });
}
// 寫入 cache
await setCachedUser(userId, user);
res.json({ source: 'db', user });
});
團隊協作要注意的事
1. 不要把 .env 提交到 Git
docker-compose.yml 裡密碼是明碼的(方便團隊共享),但 .env 應該加入 .gitignore,讓每個人自己建立:
# .env 範例(不要提交到 Git)
DATABASE_URL=mysql://admin:mysecretpassword@localhost:3306/myapp_dev
REDIS_URL=redis://localhost:6379
2. 為不同環境準備不同的 compose 檔
# 開發:啟動所有服務,掛載 source code
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
# 生產:只有必要服務,Image 來自 registry
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
3. Windows 和 macOS 的檔案效能問題
Docker Desktop 在 Windows/macOS 上的 volume 掛載效能比 Linux 差很多。如果 npm install 在掛載目錄下執行很慢,改成在容器內執行一次 npm ci,把 node_modules 存在匿名 volume:
# 在 docker-compose.yml 的 app service 加入
volumes:
- ./src:/app/src
- /app/node_modules # 匿名 volume,容器優先使用
常見問題
Q:docker compose 和 docker-compose 有什麼差別?
A:docker-compose 是舊版指令,V1 版本。docker compose(空白)是 Docker Desktop 內建的 V2 版本,指令相同但效能更好。建議用 docker compose(空白),它是 Docker 官方現在推薦的方式。
Q:MySQL 和 PostgreSQL 要選哪個?
A:如果是新專案,推薦 PostgreSQL。MySQL 的 SQL 語法與 PostgreSQL 有差異,且 PostgreSQL 的 JSON 支援、陣列型別、條件式索引更強大。但如果公司既有的服務是 MySQL,就別為了「潮流」而遷移。
Q:Redis 什麼時候該用?
A:Redis 的核心用途是快取和短期資料儲存(如登入 session、API rate limiting、排行榜)。它不是主要資料庫,不適合當作持久化資料的儲存——Redis 的資料是可以丟失的(雖然有 RDB/AOF 持久化),要當持久化儲存請用 MySQL/PostgreSQL。
Q:多個服務的 log 怎麼看?
A:用 docker compose logs -f 可以同時看所有服務的 logs,加上 --tail=100 限制行數:
docker compose logs -f --tail=100
總結:一致性是團隊開發的基礎
Docker Compose 的核心價值:讓「在我機器上能跑」變成「在所有人的機器上都能跑」。
這篇文章涵蓋的範圍:
- 完整的 Docker Compose 設定(Node.js + MySQL + Redis + phpMyAdmin)
- 開發環境與生產環境的分離策略
- 資料庫初始化與 volume 持久化
- Redis 快取實務範例
- 團隊協作的注意事項
下一步:把這套開發環境分享給你的隊友,確認他們能在 clone 專案後 docker compose up -d 就能開始開發。這是 Docker Compose 價值的最好證明。
本篇是「前端工程師後端入門」系列第七篇。系列前六篇:《從 Node.js 原生 HTTP 到 Express 框架》、《RESTful API 設計:前端視角的 API 思維》、《Prisma ORM:前端工程師的第一堂資料庫課》、《Docker 部署入門:把你的應用打包上線》、《GitHub Actions CI/CD:自動化你的部署流程》、《SRE 基礎概念:監控、日誌、錯誤追蹤一次上手》。