# 📋 初探 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. **持續優化**:系統可以持續改進