Part 1
做完之後,整個系統長這樣
一個 repo、一個 domain,同時跑多個年份的新生網站。使用者看到的是同一個網站,Nginx 根據 URL 路徑把請求分給對應年份的前端。
🌐 使用者瀏覽器
ncufresh.ncu.edu.tw
▼
▼
路徑對應
/ → 2026 前端
Container: frontend-2026
/25/ → 2025 前端
Container: frontend-2025
/24/ → 2024 前端
Container: frontend-2024
▼
統一後端(Express)
Container: backend
▼
MariaDB多個 database(ncufresh24, ncufresh25, ncufresh26)
RedisSession / Cache
重點:前端每年一個獨立 container,因為每年的 Nuxt 版本、設計、頁面結構都不同。但後端只有一個 container,用 route prefix 區分年份。資料庫也只有一個 MariaDB container,裡面開不同的 database。
Part 2
Repo 的目錄結構
整個 monorepo 的最終樣子:
ncufresh/
├── base/
│ ├── composables/
│ │ ├── useAuth.ts
│ │ └── useApi.ts
│ ├── middleware/
│ │ └── auth.ts
│ └── nuxt.config.ts
│
├── sites/
│ ├── 2026/
│ │ ├── pages/
│ │ ├── components/
│ │ ├── nuxt.config.ts
│ │ ├── package.json
│ │ └── Dockerfile
│ ├── 2025/
│ │ ├── pages/
│ │ ├── nuxt.config.ts
│ │ ├── package.json
│ │ └── Dockerfile
│ └── 2027/
│
├── backend/
│ ├── routes/
│ │ ├── auth.route.js
│ │ ├── blog.route.js
│ │ ├── 25/
│ │ └── 24/
│ └── Dockerfile
│
├── nginx/
│ └── nginx.conf
├── docker-compose.yml
├── init-db.sql
└── .gitlab-ci.yml
Part 3
關鍵問題解答
Docker 會同時跑好幾個 container 嗎?不能合併?
前端:每年一個 container,不合併。因為每年的 Nuxt 版本不同(25 是 Nuxt 3、26 是 Nuxt 4)、npm 依賴不同、build 出來的東西完全不同,硬合在一起只會製造問題。
後端:只有一個 container。所有年份的 API route 都跑在同一個 Express app 裡,用路徑前綴(/api/25/、/api/26/)區分。
資料庫:只有一個 MariaDB container。裡面開多個 database(ncufresh25、ncufresh26),資料完全隔離,但共用同一個 DB server。
實際跑起來的 container 列表:
docker compose ps
frontend-2026Nuxt 4 · port 3000
frontend-2025Nuxt 3 · port 3000
frontend-2024Nuxt 3 · port 3000
backendExpress · 統一處理所有年份的 API
MariaDB內含 ncufresh24, ncufresh25, ncufresh26 三個 DB
RedisSession & Cache
Nginx反向代理 · 路徑分流
每個前端 container 內部都監聽 port 3000,但它們是獨立的 Docker container,彼此不衝突。Nginx 根據 URL 路徑決定把請求送到哪個 container。
前端都在 ncufresh.ncu.edu.tw 底下,localStorage 和 Pinia 會衝突嗎?
短答:Pinia 不會衝突,localStorage 會,但加上 namespace 就解決了。
先搞清楚一件事:雖然 2025 和 2026 的前端跑在不同路徑(/25/ vs /),但它們的 domain 都是 ncufresh.ncu.edu.tw。在瀏覽器的眼中,這是同一個 origin。
瀏覽器儲存機制與衝突風險
localStorage — ⚠️ 共用,會衝突
同一個 origin 下所有路徑共享同一個 localStorage。如果 2025 和 2026 都存了一個叫 token 的 key,它們會互相覆蓋。
sessionStorage — ⚠️ 共用(同分頁時)
同理,同一個瀏覽器分頁裡如果從 /25/ 導航到 / 也會共享。
Cookies — ⚠️ 預設共用
Cookie 預設 path 是 /,所有年份都看得到。可以用 path 屬性限縮。
Pinia(記憶體狀態)— ✅ 不衝突
每個年份是獨立的 Nuxt app,Pinia store 活在各自的 JavaScript 記憶體裡,互不干擾。
解法:加年份前綴(namespace)。把所有寫入 localStorage 的 key 加上年份標記,避免衝突。
// ❌ 會衝突 — 2025 和 2026 都寫同一個 key
localStorage.setItem('token', jwtToken)
localStorage.setItem('user', JSON.stringify(userData))
// ✅ 加年份前綴,完全隔離
const YEAR = '2026'
localStorage.setItem(`ncufresh_${YEAR}_token`, jwtToken)
localStorage.setItem(`ncufresh_${YEAR}_user`, JSON.stringify(userData))
特別注意 pinia-plugin-persistedstate — 如果你有用這個套件把 Pinia store 持久化到 localStorage,它預設的 key 是 store 的 id(例如 user)。兩個年份的 store 如果 id 一樣就會互蓋。解法是在 persist 設定裡自訂 key:
// sites/2026/stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({ name: '', token: '' }),
persist: {
key: 'ncufresh-2026-user', // ← 自訂 key,不會和其他年份衝突
},
})
Part 4
新的一年要做什麼?
假設現在是 2027 年,要開新一屆的新生網站。以下是完整步驟。
把上一年的前端「降級」到子路徑
2026 目前跑在根路徑 /。加一行設定把它搬到 /26/,讓出根路徑給 2027。修改 sites/2026/nuxt.config.ts,加上 app: { baseURL: '/26/' }。同時更新 Nginx 設定,加一個 location /26/ 的 block。
建立 2027 的前端
在 sites/ 下建立 2027/ 資料夾。可以從 2026 複製骨架,或從零開始。重點是 nuxt.config.ts 裡要 extends: ['../../base'] 來繼承共用的 composables 和 middleware。根路徑 / 留給它。
後端加上 2027 的 routes
在 backend/routes/ 下建立 27/ 資料夾,寫新的 route handlers。掛到 app.use('/api/27/', ...)。如果業務邏輯沒變,可以直接複用 26 的 handler 只改 DB 連線。
開一個新的 Database
在 init-db.sql 加一行 CREATE DATABASE IF NOT EXISTS ncufresh27;。跑 migration 建表。
更新 docker-compose.yml
加一個 frontend-2027 service,指向 sites/2027/Dockerfile。Nginx 設定的 location / 改為 proxy 到 frontend-2027。
更新 CI/CD
在 .gitlab-ci.yml 加 frontend-2027-build job,用 changes 條件限定只在 sites/2027/**/* 或 base/**/* 改動時觸發。
每年重複的工作量其實不大——核心就是:建資料夾、寫設定、加 routes、開 DB、更新 docker-compose。基礎設施(Nginx、CI、base layer)第一次建好後就不太需要動了。
總結
一張圖看完整個架構邏輯
前端 — 各年獨立
每年一個 Nuxt app、一個 Docker container、一個子路徑。最新年度佔根路徑 /,舊年份依序退到 /26/、/25/、/24/。它們共享 base/ layer 裡的 composables 和 middleware,但頁面、元件、樣式完全獨立。
後端 — 統一一個
一個 Express app 處理所有年份的 API。用 route prefix 區分(/auth/ 給最新年度,/api/25/、/api/24/ 給舊年份)。OAuth callback 透過 state 參數判斷年份,導回正確的前端路徑。
資料庫 — 一個 server,多個 database
同一個 MariaDB instance 裡開多個 database(ncufresh24、ncufresh25、ncufresh26),資料完全隔離。不需要跑多個 DB container。
瀏覽器儲存 — 需要 namespace
因為所有年份都在同一個 domain 下,localStorage 是共享的。所有寫入 localStorage 的 key 都要加年份前綴(如 ncufresh_2026_token),Pinia 的 persistedstate 也要自訂 key。記憶體中的 Pinia state 不會衝突。