# 📋 初探 Docker Render 環境部署 CI/CD
> 基於 Next.js 15 + React 19 + TypeScript 的全端 To-Do List 應用開發練習
> Github: https://github.com/Yo0GuitarIT/to-do-list-2025
> Demo: https://to-do-list-2025.onrender.com
## 📖 目錄
- 1. 建立基礎應用
- 2. 數據持久化設計
- 3. 加入測試
- 4. Docker 容器化
- 5. 建置 CI/CD
- 6. 部署上 Render 雲端
- 7. 學習成果總結
---
## 1. 建立基礎應用
### 🏗️ 專案初始化
**Git 提交歷程:**
```bash
bc9ffec Initial commit from Create Next App
b66538c feat: reset style 移除 globals.css 和 page.tsx 中的多餘樣式與內容,簡化結構
```
### ⚙️ 關鍵配置
**Next.js 配置為 Docker 部署做準備:**
```typescript
// next.config.ts
const nextConfig: NextConfig = {
output: "standalone", // 關鍵:為 Docker 部署啟用獨立模式
serverExternalPackages: ["@prisma/client", "prisma"],
};
```
### 💡 重點注意事項
1. **standalone 模式的重要性**
- 減少 Docker 映像大小(從 GB 級降到 MB 級)
- 包含所有必要依賴,無需 node_modules
- 生產環境的關鍵優化
2. **清理預設內容的原因**
- 避免不必要的樣式衝突
- 為自定義設計提供乾淨基礎
- 減少專案複雜度
---
## 2. 數據持久化設計
### 🗄️ 資料庫選擇挑戰
**遇到的問題:**
- 本地沒有安裝 PostgreSQL
- 不想在系統安裝額外軟體
- 需要保持開發環境乾淨
**解決方案:Docker 容器化 PostgreSQL**
### 📊 Prisma Schema 設計
```prisma
// prisma/schema.prisma - 簡潔的數據模型
model Todo{
id Int @id @default(autoincrement())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("todos") // 映射到 todos 表
}
```
### 🔌 Prisma 客戶端單例模式
```typescript
// src/lib/prisma.ts - 避免開發環境重複連接
const globalForPrisma = globalThis as unknown as{
prisma: PrismaClient| undefined;
}
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma; // 開發環境重用連接
}
```
### ⚠️ 關鍵問題與解決
**1. 開發階段的手動管理問題:**
```bash
# 早期的麻煩操作
docker run --name postgres-todo -e POSTGRES_USER=todouser ... -d postgres:17
npm run dev # 需要兩個終端
```
**問題點:**
- 每次都要手動啟動 PostgreSQL 容器
- 容器停止後數據可能遺失
- 團隊成員環境不一致
**2. Prisma 連接池問題:**
- Next.js 開發模式會重新載入模組
- 造成多個 Prisma 客戶端實例
- 解決:使用全局變數保持單例
### 💡 重點注意事項
1. **環境變數管理**
- 本地開發使用 `.env.local`
- 生產環境使用平台環境變數
- 絕不將敏感資訊提交到 Git
2. **資料庫連接字串格式**
```
DATABASE_URL="postgresql://user:password@host:port/database"
```
---
## 3. 加入測試
### 🧪 測試工具選擇考量
**為什麼選擇 Vitest 而非 Jest:**
- 與 Vite 生態系統整合更好
- 更快的測試執行速度
- 原生 ESM 支援
- 更好的 TypeScript 支援
### 📋 三層測試架構
**測試策略:**
```
頁面測試 (E2E-like) ↑ 少量但重要
組件測試 (Integration) ↑ 中等數量
API 測試 (Unit) ↑ 大量且詳細
```
### ⚠️ 測試遇到的問題
**1. Next.js 模組 Mock 困難:**
```typescript
// 解決方案:在 setup.ts 中統一 Mock
vi.mock("next/router", () => ({
useRouter: () => ({ push: vi.fn(), pathname: "/" }),
}));
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), pathname: "/" }),
}));
```
**2. Prisma Mock 策略:**
```typescript
// 直接 Mock 整個 Prisma 模組
const mockPrisma = {
todo: {
findMany: vi.fn(),
create: vi.fn(),
// ... 其他方法
},
};
vi.mock("@/lib/prisma", () => ({ prisma: mockPrisma }));
```
**3. 異步測試的等待問題:**
- 使用 `waitFor` 等待異步操作完成
- 設定合理的 timeout
- 避免使用 `act` 包裝(React 18+ 自動處理)
### 💡 重點注意事項
1. **測試覆蓋率 vs 測試品質**
- 重視關鍵路徑測試
- 包含錯誤處理測試
- 測試使用者實際操作流程
2. **Mock 策略**
- 只 Mock 外部依賴
- 保持測試的可讀性
- 避免過度 Mock 導致測試失去意義
---
## 4. Docker 容器化
### 🏗️ 容器架構決策
**選擇多容器分離架構的原因:**
- 符合微服務架構原則
- 可以獨立擴展和維護
- 雲端部署更靈活
### 📦 Dockerfile 多階段建置
```dockerfile
# 關鍵階段
FROM node:18-alpine AS deps # 安裝依賴
FROM base AS builder # 建置應用 + 生成 Prisma
FROM base AS runner # 精簡運行環境
# 安全性考量
RUN adduser --system --uid 1001 nextjs
USER nextjs # 非 root 用戶運行
```
### 🐳 Docker Compose 配置重點
```yaml
# 關鍵配置
services:
db:
image: postgres:17-alpine
healthcheck: # 確保資料庫就緒
test: ["CMD-SHELL", "pg_isready -U todouser -d todolist"]
app:
depends_on:
db:
condition: service_healthy # 等待資料庫健康
```
### ⚠️ 容器化過程中的問題
**1. 資料庫啟動順序問題:**
```bash
# 問題:應用在資料庫準備好之前啟動
# 解決:使用 healthcheck 和 depends_on
```
**2. Prisma 生成問題:**
```dockerfile
# 問題:運行階段找不到 Prisma 客戶端
# 解決:在 builder 階段生成,然後複製到 runner
RUN npx prisma generate
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
```
**3. 環境變數傳遞問題:**
```yaml
# 注意:容器內的主機名稱
DATABASE_URL=postgresql://todouser:todopassword@db:5432/todolist
# ^^
# 服務名稱,非 localhost
```
**4. 資料持久化問題:**
```yaml
# 解決:使用 Docker volumes
volumes:
- postgres_data:/var/lib/postgresql/data
```
### 🔄 開發體驗轉變
**轉變過程:**
```bash
# 之前:多步驟手動操作
docker run postgres...
npm run dev
# 之後:一鍵啟動
docker-compose up
```
**帶來的好處:**
- 環境一致性:團隊成員使用相同環境
- 簡化操作:一條指令啟動所有服務
- 隔離性:不污染本地系統
```
┌─────────────────┐ ┌──────────────────┐
│ todo-app │────│ todo-postgres │
│ (Web Service) │ │ (Database) │
│ - Docker │ │ - Managed PG │
│ - Next.js │ │ - Auto backup │
│ - Port 3000 │ │ - SSL enabled │
└─────────────────┘ └──────────────────┘
```
### 💡 重點注意事項
1. **Docker 映像大小優化**
- 使用 Alpine Linux 基礎映像
- 多階段建置移除開發依賴
- 啟用 Next.js standalone 模式
2. **安全性考量**
- 使用非 root 用戶
- 最小權限原則
- 不在映像中包含敏感資訊
3. **網路配置**
- 容器間使用服務名稱通信
- 適當的埠口映射
- 網路隔離
---
## 5. 建置 CI/CD
### 🔧 CI/CD 流程設計
**設計思路:**
```
品質檢查 → 測試 → 建置 → 部署
↓ ↓ ↓ ↓
ESLint Vitest Build Render
```
### ⚠️ CI/CD 建置過程中的重大問題
**Git 提交歷程反映的除錯過程:**
```bash
6dda445 feat: add CI/CD pipeline with testing, building, and deployment steps
f1715fe fix: add load parameter to make Docker image available for testing ← 關鍵問題
1f76780 fix: improve Docker testing in GitHub Actions workflow
bfd6949 fix: resolve ESLint errors in GitHub Actions
34ed36f refactor: update cicd
```
### 🐛 具體問題與解決方案
**1. Docker 映像在 CI 中不可用問題:**
```yaml
# 問題:build 後的映像無法在同一工作流中使用
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
# 缺少這個參數導致映像無法載入
# 解決方案:
with:
context: .
load: true # 關鍵修復!讓映像可用於本地測試
tags: todo-app:test
```
**問題原因:**
- `docker/build-push-action` 預設不會載入映像到本地
- 只是建置並推送到 registry
- 需要 `load: true` 讓映像在 runner 中可用
**2. PostgreSQL 服務協調問題:**
```yaml
# 問題:應用容器無法連接到 GitHub Actions 的 PostgreSQL 服務
# 解決:正確配置 healthcheck 和網路
services:
postgres:
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
```
**3. ESLint 配置在 CI 環境的嚴格性:**
```javascript
// 問題:本地開發忽略的格式問題在 CI 中失敗
// 解決:調整 ESLint 配置
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
{
ignores: ["src/test/vitest.d.ts"], // 忽略測試類型定義
},
];
```
**4. 環境變數在不同階段的管理:**
```yaml
# CI 測試環境
env:
DATABASE_URL: postgresql://testuser:testpassword@localhost:5432/testdb
# 部署階段使用 Render 提供的環境變數
# 通過 secrets 管理敏感資訊
```
### 🔄 除錯過程學習
**真實的開發體驗:**
1. **初次建置**:基本功能能跑,但有隱藏問題
2. **第一次失敗**:Docker 映像無法使用 → 加入 `load: true`
3. **第二次失敗**:測試環境配置問題 → 調整 healthcheck
4. **第三次失敗**:ESLint 嚴格檢查 → 修復程式碼規範
5. **重構優化**:流程穩定後進行效率優化
### 💡 重點注意事項
1. **CI/CD 的漸進式建置**
- 先讓基本流程跑通
- 逐步解決出現的問題
- 不要一次做太多變更
2. **環境差異處理**
- 本地開發 vs CI 環境的差異
- 網路配置不同
- 資源限制不同
3. **錯誤訊息解讀**
- GitHub Actions 的日誌很詳細
- 重點關注 exit code 和錯誤訊息
- 分階段除錯,不要同時改多個地方
---
## 6. 部署上 Render 雲端
### ☁️ Render 選擇考量
**為什麼選擇 Render:**
- 有免費方案適合學習
- 原生支援 Docker
- 自動 SSL 證書
- 簡單的環境變數管理
- 與 GitHub 整合良好
### 🔧 Render 配置策略
```yaml
# render.yaml - Infrastructure as Code
services:
- type: web
name: todo-app
runtime: docker # 直接使用我們的 Dockerfile
envVars:
- key: DATABASE_URL
fromDatabase: # 自動從資料庫服務獲取連接字串
name: todo-postgres
property: connectionString
- type: pserv # 託管 PostgreSQL
name: todo-postgres
plan: free
```
### ⚠️ 部署過程中的關鍵問題
**1. Auto Deploy 的控制問題:**
```
問題:Render 預設會自動部署每次 Git 推送
影響:與 GitHub Actions CI/CD 衝突
解決:關閉 Render 的 Auto Deploy,改用 API 控制
```
**2. 環境變數管理:**
```yaml
# 問題:本地和生產環境的環境變數不同
# 解決:使用 Render 的環境變數自動注入
envVars:
- key: DATABASE_URL
fromDatabase:
name: todo-postgres
property: connectionString # Render 自動生成正確的連接字串
```
**3. Docker 建置 Context 問題:**
```yaml
# 確保 Dockerfile 路徑正確
dockerfilePath: ./Dockerfile # 相對於 repository 根目錄
```
**4. 健康檢查配置:**
```yaml
# Render 需要知道如何檢查應用是否健康
healthCheckPath: / # 檢查根路徑是否回應 200
```
### 🚀 自動部署整合
**GitHub Actions 與 Render API 整合:**
```yaml
# CI/CD 流程
質量檢查 → 測試 → 建置 → (如果是 main 分支) → 觸發 Render 部署
```
**關鍵配置:**
```yaml
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: 🚀 Trigger Render deployment
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}" \
"https://api.render.com/v1/services/${{ secrets.RENDER_SERVICE_ID }}/deploys"
```
### 🔐 Secrets 管理
**需要設定的 GitHub Secrets:**
```
RENDER_API_KEY=rnd_xxx... # Render API 金鑰
RENDER_SERVICE_ID=srv-xxx... # Render 服務 ID
```
**取得方式:**
1. Render Dashboard → Account Settings → API Keys
2. Render Dashboard → Service → Settings → Service ID
### 💡 重點注意事項
**1. 分階段部署策略:**
- 先手動部署確認基本功能
- 再整合自動化 CI/CD
- 避免一次性整合太多變數
**2. 監控和除錯:**
- Render 提供詳細的建置和運行日誌
- 注意資源使用限制(免費方案有限制)
- 設定適當的健康檢查
**3. 成本考量:**
- 免費方案的限制和休眠機制
- 資料庫的連接數限制
- 適合學習和展示,生產環境需評估
---
## 7. 學習成果總結
### 🎯 技術成長軌跡
**從簡單到複雜的學習路徑:**
```
基礎應用 → 資料庫整合 → 測試建立 → 容器化 → CI/CD → 雲端部署
↓ ↓ ↓ ↓ ↓ ↓
React Prisma Vitest Docker GitHub Render
基礎 ORM 測試 容器 Actions 雲端
```
### 🏆 關鍵決策與學習
**1. 技術選擇的考量:**
- **Next.js 15**:最新功能,學習前沿技術
- **Prisma**:類型安全的 ORM,提升開發效率
- **Docker**:解決環境一致性問題
- **Render**:簡單易用的雲端平台
**2. 架構決策的智慧:**
- **多容器分離**:而非單體容器
- **多階段建置**:優化映像大小和安全性
- **漸進式測試**:三層測試金字塔
- **自動化部署**:減少人為錯誤
### 🚧 遇到的挑戰與收穫
**主要挑戰類別:**
1. **環境一致性問題**
- 本地 vs 容器 vs CI vs 生產環境
- 學會:環境變數管理、Docker 網路配置
2. **工具整合複雜性**
- Next.js + Prisma + Docker + GitHub Actions + Render
- 學會:分階段除錯、問題隔離
3. **CI/CD 調校困難**
- Docker 映像載入、PostgreSQL 服務協調
- 學會:讀懂錯誤訊息、漸進式修復
### 📈 實際應用價值
**完整的現代化開發流程:**
```mermaid
graph LR
A[本地開發] --> B[程式碼推送]
B --> C[自動測試]
C --> D[自動建置]
D --> E[自動部署]
E --> F[線上服務]
```
**學會的核心技能:**
- ✅ **全端開發**:前後端整合開發
- ✅ **DevOps 實務**:從開發到部署的完整鏈路
- ✅ **問題診斷**:系統性解決複雜問題
- ✅ **工具整合**:多種工具的協調使用
### 🔮 後續發展方向
**可以延伸的學習:**
1. **效能優化**:快取策略、資料庫優化
2. **安全強化**:身份驗證、授權機制
3. **監控告警**:APM 工具、日誌分析
4. **擴展功能**:實時更新、離線支援
### 💡 關鍵心得
**最重要的學習:**
1. **漸進式開發**:不要一次做太多變更
2. **問題隔離**:分層次解決問題
3. **文檔記錄**:記錄每個決策的原因
4. **持續優化**:系統可以持續改進