chore(openspec): 新增 bake-image-remove-bind-mount change 規劃
P0「移除 Docker bind mount、程式碼烘進 image」的完整規劃:Dockerfile 改 multi-stage(app + web)、docker-compose.yml 拿掉五個服務的 bind mount、nginx 改本地 build 取得 public/、deploy.yml 改 build-then-up、 entrypoint 職責收斂、.dockerignore 補齊。尚未實作,僅規劃文件。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142LC1kQb8EyV59TjHDkhWS
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-04
|
||||
@@ -0,0 +1,136 @@
|
||||
## Context
|
||||
|
||||
`docker-compose.yml` 目前對 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 五個服務都掛 `./:/var/www` bind mount,`Dockerfile` 完全沒有 `COPY . /var/www`。這代表:
|
||||
|
||||
- build 出來的 `cfdive-platform` image 是空殼,不含 `app/`、`routes/`、`vendor/`,無法脫離主機獨立運作。
|
||||
- 部署(`.gitea/workflows/deploy.yml`)流程是 `git reset --hard` 改寫主機檔案 → `docker compose exec app composer install`(裝在 bind mount 出去的目錄上)→ artisan cache 系列,全程沒有 `docker compose build`。
|
||||
- `docker/php/docker-entrypoint.sh` 承擔了大量「假設容器內是主機目錄」的邏輯:`.env` 不存在就從 `.env.example` 複製並 `key:generate`、composer.lock 內容比對決定是否重裝 vendor。
|
||||
- 本機開發長期依賴 bind mount 做到「改 code 立即生效」(後端用 `kill -USR2 1` 讓 OPcache 重新驗證,前端 Vite HMR)。
|
||||
|
||||
P1(log 改 stderr、session 改 Redis)與 `file-storage-s3`(上傳檔案可切 S3,但 VPS 尚未真的切換,`.env` 仍是 `FILESYSTEM_DISK=local`)已經拆掉了「container 必須綁死主機磁碟」的兩個主要理由(log 與可選的檔案儲存),這次要處理最後一塊:程式碼本身。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- production(VPS 實際部署用)的 `docker-compose.yml` 不再依賴主機 bind mount 取得程式碼,image build 完成即可獨立運作。
|
||||
- 部署流程改為每次 `git pull` 後重新 `docker compose build`,取代現在的「改主機檔案 + exec 裝套件」。
|
||||
- `.env` 改用 `env_file:` 傳遞,不再依賴 bind mount 把整個目錄(含 `.env`)帶進容器。
|
||||
- 本機開發體驗(改 code 立即生效)不能被破壞——透過既有的 `compose.override.yml` 機制(`compose-cloud-baseline` 已建立此慣例)讓開發環境疊加 bind mount,production compose 本身不含。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不強制本次一併把 VPS 正式切到 `FILESYSTEM_DISK=s3`(`file-storage-s3` 程式邏輯已完成,但帳號申請與正式切換是獨立決策,留給後續變更)。
|
||||
- 不處理零停機(blue-green / rolling)部署——`docker compose build && up -d` 期間服務仍會有短暫中斷,此為已知取捨,不在本次解決範圍。
|
||||
- 不改動 `frontend/` 的既有 build 流程(`frontend` 服務本來就是獨立 build、不掛 bind mount,不受影響)。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Dockerfile 統一策略:先 copy composer manifests、install vendor,再 copy 全專案
|
||||
在 `docker-php-ext-install` 之後、`ENTRYPOINT` 之前,依序:(1) `COPY composer.json composer.lock /var/www/`,(2) `RUN composer install --no-dev --optimize-autoloader --no-interaction --no-scripts`,(3) `COPY . /var/www`,(4) 重新執行 `chown -R www-data:www-data` 與 `storage`/`bootstrap/cache` 權限設定(COPY 進來的檔案預設是 root)。這個順序讓 Docker layer cache 生效——只要 `composer.json`/`composer.lock` 沒變動,重建 image 時 `composer install` 這層可以直接命中快取、不重跑,只有應用程式碼變動的部分需要重新 COPY。**這是本次唯一策略,不與其他順序並存**(先前草稿一度在 Risk 段落與此處出現順序不一致,已統一)。
|
||||
|
||||
**替代方案考慮**:用 multi-stage build(composer stage 只複製 vendor 進最終 image)——本次不採用,因為專案規模小、build 時間不是痛點,multi-stage 增加的複雜度不值得;先用單階段搭配上述 layer cache 順序,未來若 image 太大或 build 太慢再考慮 multi-stage。
|
||||
|
||||
**`.dockerignore` 是本次的必要項目,不是可有可無的優化**:`COPY . /var/www` 會把 build context 內所有內容複製進 image,若無 `.dockerignore` 排除,`vendor/`(本機可能已存在、與容器內版本不一致)、`node_modules/`、`.env`(含資料庫密碼等機敏資訊)、`storage/app/public/*`(本機開發時已上傳的檔案,不應烘進 image)都會被複製進去,造成 image 內容錯誤或機敏資訊外洩進 image layer(即使之後刪除檔案,內容仍留在較早的 layer 歷史中)。新增 `.dockerignore` 排除上述路徑,屬於本次變更的正確性要求。
|
||||
|
||||
**composer scripts 驗證結果,`COPY . /var/www` 之後需補執行的步驟**:查證 `composer.json` 的 `scripts` 區塊,確認:
|
||||
- `post-autoload-dump` 定義了 `Illuminate\Foundation\ComposerScripts::postAutoloadDump` 與 `@php artisan package:discover --ansi`——這兩者依賴 Laravel 應用程式碼(`artisan`、`bootstrap/`)已存在才能執行,但 Decision 1 的 `composer install` 發生在 `COPY . /var/www` **之前**(只有 composer manifests),此時執行會失敗,因此必須用 `--no-scripts` 跳過。
|
||||
- `post-update-cmd`、`post-root-package-install`、`post-create-project-cmd` 是 `composer update`/`create-project` 才會觸發的 hook,一般 `composer install` 不會執行,不受影響,不需處理。
|
||||
- 因此 Dockerfile 在 `COPY . /var/www` **之後**必須手動補執行 `composer dump-autoload --optimize`(重新掃描此時才存在的 `app/` 目錄,產生完整的 optimized classmap;`--no-scripts` 之前產生的 classmap 缺少專案自身 `App\` 命名空間下的類別)與 `php artisan package:discover --ansi`(等效於原本 `post-autoload-dump` 會執行的服務提供者自動發現,寫入 `bootstrap/cache/packages.php`/`services.php`),否則跳過的 `--no-scripts` 造成的缺口不會被補上。**修正**:這裡不再重複加 `--no-dev`——`--no-dev` 是控制 `composer install` 階段要不要把 dev 依賴裝進 `vendor/` 的旗標,先前的 `composer install --no-dev` 已經決定了 vendor 內容不含 dev 套件;後續的 `dump-autoload` 只是重新掃描「已經裝好的 vendor 內容」+這次新出現的 `app/` 目錄來產生 classmap,重複加 `--no-dev` 是多餘的,拿掉能讓兩個指令的職責更清楚(install 決定裝什麼、dump-autoload 只重新產生對應的 classmap),也符合 Laravel/Docker 社群常見的分層 build 慣例。
|
||||
|
||||
### 2. Production compose 移除 bind mount,開發環境靠既有版控的 `compose.override.yml` 疊加
|
||||
`docker-compose.yml`(production 用)移除 app/nginx/scheduler/queue-worker/reverb 的 `./:/var/www`。**修正**:`compose.override.yml` 實際上**已經進版控**(`git ls-files` 確認追蹤中,內含 `phpmyadmin`/`mailpit`,`compose-cloud-baseline` spec 已規範其用途)——先前草稿誤判為「被 `.gitignore` 排除、不進版控」(`.gitignore` 排除的是另一個沒人使用的檔名 `docker-compose.override.yml`),已更正。既然是既有已版控檔案,直接在其中新增這五個服務的 `./:/var/www` bind mount 即可,不需要額外的 `.example` 範本檔案——這個檔案本身就是所有開發者共用的標準範本。開發者本機執行 `docker compose up`(不指定 `-f`)時自動疊加、維持「改 code 立即生效」的體驗;VPS 執行 `docker compose -f docker-compose.yml up -d`(已是現行慣例,`compose-cloud-baseline` 已要求正式環境不吃 override)不受影響。
|
||||
|
||||
**替代方案考慮**:改用 volume mount 只掛特定子目錄(如只掛 `app/`、`resources/`)——本次不採用,因為切分維護成本高且容易漏掉新增目錄,不如整個目錄一起在 override 掛/不掛。
|
||||
|
||||
### 3. 上傳檔案改用具名 volume `storage-app-public` 過渡,不強制切 S3
|
||||
新增具名 volume,命名為 `storage-app-public`(定名,不再是待決項目),掛載至 `app` 與 `nginx` 服務容器內的 **`/var/www/storage/app/public`**(絕對路徑,明確指定,避免與相對路徑寫法混淆)這一層路徑,讓使用者上傳的檔案在 image 重建後仍保留。
|
||||
|
||||
**為什麼不強制先切 S3**:R2 帳號申請與正式切換是獨立於本次「拿掉 bind mount」的決策(`file-storage-s3` 已記錄暫緩),不應該把兩個決策綁在一起、互相卡進度。具名 volume 是本次的暫時解法,日後若正式切到 S3,這個 volume 可以直接移除(`storage/app/public` 不再需要持久化,因為檔案都在 S3 上)。
|
||||
|
||||
**替代方案考慮**:把 `storage/app/public` 也一起 COPY 進 image——不採用,因為使用者上傳的檔案是執行期產生的資料,不該烘進 image(每次 rebuild 會用 build context 裡的舊內容覆蓋掉 volume 資料,或造成 image 越滾越大)。
|
||||
|
||||
### 4. nginx 的程式碼來源:改用 Dockerfile multi-stage 的獨立 build target,不依賴任何執行期共享機制
|
||||
**這是先前草稿的一個實質缺漏**:拿掉 `./:/var/www` bind mount 後,`nginx` 服務(原本用官方 `nginx:alpine` image + bind mount 取得 `/var/www/public`)完全沒有其他方式取得 Laravel 的 `public/` 目錄——先前草稿只處理了 app/scheduler/queue-worker/reverb 四個跑 `cfdive-platform` image 的服務,遺漏了 nginx 這個用不同 base image、且職責只是靜態檔案服務+反向代理的服務。
|
||||
|
||||
**決策**:把 `Dockerfile` 改為 multi-stage:
|
||||
1. 第一階段 `FROM php:8.2-fpm AS app`(即 Decision 1 描述的既有內容:composer install → COPY . /var/www → 建立 `public/storage` 符號連結,見下方符號連結策略)。
|
||||
2. 新增第二階段 `FROM nginx:alpine AS web`,執行 `COPY --from=app /var/www/public /var/www/public` 把 `app` 階段 build 完成的 `public/` 目錄複製進 nginx 專用的 image,再 `COPY docker/nginx/conf.d/ /etc/nginx/conf.d/` 帶入既有 nginx 設定。
|
||||
3. `docker-compose.yml` 的 `nginx` 服務改為 `build: {context: ., dockerfile: Dockerfile, target: web}`(而非 `image: nginx:alpine` 直接從 registry 拉取),`image:` 欄位命名為 `cfdive-nginx-web`(與既有 `cfdive-frontend`——Vue/Vite 前端——區分,避免混淆)。
|
||||
|
||||
**為什麼不用共享 volume 讓 nginx 讀 app 的 `public/`**:具名 volume 一旦第一次由某容器掛載並帶入初始內容,之後即使 image 重新 build(新程式碼、新前端資源),volume 內容不會自動更新(Docker 只在 volume 為空時才從 image 複製一次)——這會導致「重新部署後 nginx 仍在服務舊的靜態資源」的嚴重 bug。改用 `COPY --from=app` 的 multi-stage 方式,`web` 階段的 `public/` 內容是每次 `docker compose build` 都重新從 `app` 階段複製的,沒有這個過期風險。
|
||||
|
||||
**與 Decision 1「不採用 multi-stage」的關係**:Decision 1 拒絕的是「composer stage 只複製 vendor 進最終 image」這種為了縮小 app image 體積的 multi-stage 優化,理由是複雜度不值得。這裡的 multi-stage 是為了解決「nginx 需要 app 的 public/ 內容」這個功能性缺口,屬於不同目的,兩者不衝突——app 階段本身仍是單一 `RUN composer install` 的簡單流程,沒有引入 vendor 相關的 multi-stage 複雜度。
|
||||
|
||||
### 5. `public/storage` symlink 策略:app 與 web 兩階段各自獨立建立,不依賴跨階段繼承
|
||||
**這是先前草稿的另一個缺漏**:原草稿假設「entrypoint 在 app container 內執行 `storage:link` 之後,nginx 就看得到」——這個假設是錯的,`app` 與 `nginx` 是兩個完全獨立的容器檔案系統,一個容器內建立的符號連結不會出現在另一個容器裡。
|
||||
|
||||
**決策(修正版)**:`config/filesystems.php` 的 `links` 設定只有單一固定項目(`public_path('storage') => storage_path('app/public')`),對應的符號連結是一個不依賴執行期狀態、內容固定的相對路徑連結(`public/storage -> ../storage/app/public`)。改用純檔案系統操作、在 **build 階段**建立,且 `app` 與 `web` 兩個階段**各自獨立執行一次**,不依賴 `COPY --from=app` 把 `app` 階段建立的符號連結隱性帶到 `web` 階段:
|
||||
- `app` 階段於 `COPY . /var/www` 之後執行 `RUN ln -sfn ../storage/app/public /var/www/public/storage`
|
||||
- `web` 階段於 `COPY --from=app /var/www/public /var/www/public` 之後**再執行一次同樣的指令** `RUN ln -sfn ../storage/app/public /var/www/public/storage`
|
||||
|
||||
**為什麼不依賴 `COPY --from` 隱性繼承**:雖然 Docker `COPY --from` 技術上會保留符號連結本身(不解析成實體檔案),讓 `web` 階段「順帶」拿到 `app` 階段建立的符號連結,但這是一個隱性、容易被日後維護者忽略的耦合——如果之後有人調整 `COPY --from` 的複製範圍(例如改成只複製特定子目錄、或換成其他複製方式),這個符號連結就會無聲消失且不易察覺。改成兩階段**各自明確宣告**同一件事,即使其中一階段的建置方式改變,另一階段仍不受影響,且從 Dockerfile 原始碼就能直接看出「兩個 image 都需要這個符號連結」的意圖,不需要理解 `COPY --from` 的符號連結保留語意才能推斷。
|
||||
|
||||
**`ln -sf` 改為 `ln -sfn`**:`-n`(`--no-dereference`)避免一個常見陷阱——若目的地路徑已經是一個指向目錄的符號連結,`ln -sf`(不含 `-n`)會把新連結建立在該目錄「裡面」而非取代它。對 Docker 重建 image 的情境(layer 快取失效後重跑同一條 `RUN` 指令)加上 `-n`可確保每次執行都是冪等地取代既有符號連結,而不是意外在裡面巢狀建立。
|
||||
|
||||
**不使用 `php artisan storage:link`**:因為這只是建立一個符號連結檔案,不需要 Laravel 執行期或資料庫連線。
|
||||
|
||||
符號連結指向的實際檔案內容(`storage/app/public/` 下的檔案本身)則透過 Decision 3 的 `storage-app-public` 具名 volume,同時掛載到 `app` 與 `nginx` 容器的 `/var/www/storage/app/public` 路徑——兩個容器的符號連結都指向同一個相對路徑,該路徑在兩邊都被同一個具名 volume 覆蓋,內容一致。
|
||||
|
||||
**因此 entrypoint 不再需要執行 `storage:link`**(與先前草稿不同,見 Decision 8 更新)。
|
||||
|
||||
### 6. deploy.yml 改為 build-then-up,migration/cache 挪到新 container 啟動後
|
||||
新流程:
|
||||
```
|
||||
git reset --hard origin/master
|
||||
docker compose build
|
||||
docker compose up -d --remove-orphans
|
||||
# 等 app healthy 後:
|
||||
docker compose exec -T app php artisan migrate --force
|
||||
docker compose exec -T app php artisan config:cache / route:cache / view:cache / event:cache
|
||||
docker compose exec -T app php artisan l5-swagger:generate
|
||||
```
|
||||
`docker compose up -d` 本身會因為 image 內容改變觸發 app/scheduler/queue-worker/reverb/nginx 重新建立 container(compose 偵測 image ID 變化);`restart queue-worker` 這步可以拿掉,因為 `up -d` 已經會重建它。
|
||||
|
||||
**風險**:`docker compose build` 若失敗(例如 composer 依賴解析錯誤),現行 `git reset --hard` 已經把主機檔案改到新版本,但 container 還在跑舊 image——這與現行流程風險相同(現行流程 composer install 失敗一樣會卡在中間態),不是本次變更新增的風險,但要在 tasks 裡明確測試「build 失敗時 compose 是否維持舊 container 運作」。
|
||||
|
||||
### 7. `.env` 改用 `env_file:` 注入環境變數,範圍只給 Laravel runtime 服務,entrypoint 移除「主機目錄」假設邏輯
|
||||
**範圍修正**:`env_file:` **只加在 `app`、`scheduler`、`queue-worker`、`reverb` 四個實際執行 Laravel/PHP 的服務**,**不加在 `nginx` 服務**。`nginx` 目前完全沒有 `environment:` 設定、不需要讀取任何 `.env` 內容(DB 密碼、`APP_KEY`、`AWS_*` 等)——它的職責只是服務 `web` 階段內建的靜態檔案並反向代理 `.php` 請求到 `app:9000`。給它 `.env` 存取權屬於不必要的機敏資訊暴露(最小權限原則),也容易誤導未來維護者以為 nginx 有讀取這些變數的需求。
|
||||
|
||||
四個 Laravel runtime 服務加上 `env_file:`,**明確用 YAML list 格式**:
|
||||
```yaml
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
(不用 `env_file: .env` 這種單一純量寫法,list 格式是本次統一採用的寫法,VPS 上該檔案由既有部署流程手動維護,路徑不變)。**文案修正**:`env_file:` 的作用是把該檔案內容**以環境變數形式**注入容器程序(等同 `docker run --env-file`),**不會在容器檔案系統裡生成一份 `.env` 檔案**。先前草稿「`.env` 已由 `env_file:` 於執行期注入」這句話容易誤讀成「容器內會有一個 `.env` 檔案」,已修正措辭。若程式碼中有任何地方依賴讀取實體 `.env` 檔案(而非 `getenv()`/`env()` 讀環境變數),需額外處理,但目前 Laravel 的 `env()` 本來就是讀環境變數,本次不受影響。
|
||||
|
||||
`docker-entrypoint.sh` 移除:
|
||||
- `.env` 不存在則複製 `.env.example` + `key:generate`(`env_file:` 已確保環境變數存在,不需要容器內自動生成任何 `.env` 檔案)
|
||||
- composer.lock 內容比對後才 `composer install`(vendor 已在 image build 階段裝好,執行期不需要)
|
||||
- 強制改寫 `DB_HOST=db` 那段 `preg_replace` 邏輯(改成直接在 `docker-compose.yml` 的 `environment:` 或 `.env` 裡設定,不用執行期改寫檔案)
|
||||
|
||||
### 8. 消除 deploy 與 entrypoint 的責任重疊
|
||||
先前草稿讓 `docker-entrypoint.sh` 背景執行 migration/cache clear/swagger 生成,同時 `.gitea/workflows/deploy.yml` 又在 app healthy 後執行 migration/cache/swagger——兩邊職責重疊,且 entrypoint 這段邏輯會在 `app`、`scheduler`、`queue-worker` 三個服務啟動時各跑一次(三者都用同一個 entrypoint script,只是 `command:` 不同),造成 migration 被多個 container 併發嘗試執行的風險。改為明確切分:
|
||||
|
||||
- **deploy.yml 專責**:migration、`config:cache`/`route:cache`/`view:cache`/`event:cache`、`l5-swagger:generate`——這些是「部署一次新版本」該做的事,只需執行一次,且只在 CI/CD 流程裡由人/流程明確觸發。
|
||||
- **entrypoint 只保留啟動必要工作**:目錄與權限設定(`storage`/`bootstrap/cache`,因為 `storage-app-public` 具名 volume 掛載時會覆蓋 image 內建的權限,每次啟動都需要重新確保)。**`storage:link` 不再是 entrypoint 的工作**——依 Decision 5(修正版),符號連結已在 `app` 與 `web` 兩個階段 build 時各自獨立建立,執行期不需要再做任何事。
|
||||
- 移除 entrypoint 裡「等待 MySQL」的背景迴圈:這段原本只是為了讓後續的 migration 有 DB 可用,migration 移到 deploy.yml 後,entrypoint 不再需要等 MySQL 才能完成它剩下的工作(目錄權限設定不需要 DB)。
|
||||
|
||||
**風險**:若日後有人在 `queue-worker`/`reverb` 啟動時就需要 migration 已完成(例如 migration 會改變 queue 用到的資料表結構),deploy.yml 的「migration 在所有服務都 `up -d` 之後才執行」可能有短暫窗口讓 worker 用到舊 schema 處理任務。緩解:這與現行流程風險相近(現行 migration 也是部署腳本晚於 container 啟動才跑),且 Laravel migration 多為向下相容設計(新增欄位等),非本次變更引入的新風險,故不在本次額外處理,僅記錄於此供未來參考。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **部署變慢** → 每次 `docker compose build` 預期數十秒到數分鐘(視 composer install 網路狀況),取代現在近乎瞬間的 `git reset --hard`。緩解:Decision 1 的 layer 順序(composer manifests 先於程式碼)已利用 Docker layer cache 降低影響。
|
||||
- **本機改 code 不再瞬間生效(若忘記用 override)** → 開發者若誤用 `docker compose -f docker-compose.yml up`(不帶 override)會觸發「改 code 要 rebuild」的體驗,容易誤判為 bug。緩解:README/CLAUDE.md 補充說明本機開發指令固定用 `docker compose up`(不加 `-f`),CI/部署腳本固定用 `-f docker-compose.yml`。
|
||||
- **`storage/app/public` 具名 volume 是過渡方案,非最終架構** → 之後正式切 S3 時需要額外一個變更去移除這個 volume 並確認舊檔案已遷移(`storage:migrate-to-s3` 已存在)。緩解:本次 design 已明確標注這是暫時方案,避免未來誤以為是最終狀態。
|
||||
- **`docker compose build` 失敗時的中間態** → 現行流程本來就有「`git reset --hard` 已改主機檔案但 container 還沒套用」的窗口,本次不惡化此風險,但需在 tasks 裡加一項手動驗證。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 本機驗證:修改 `Dockerfile`、`docker-compose.yml`、調整既有已版控的 `compose.override.yml`、`docker-entrypoint.sh`,本機執行 `docker compose build && docker compose up -d` 確認所有服務正常啟動、`nc -z 127.0.0.1 9000` healthcheck 通過、既有測試套件全綠。
|
||||
2. 本機驗證開發體驗:確認 `compose.override.yml` 生效後改 PHP 檔案不需 rebuild 即可看到效果(沿用現行 `kill -USR2 1` 或容器重啟機制)。
|
||||
3. 更新 `.gitea/workflows/deploy.yml` 為 build-then-up 流程,先在非 master 分支(或手動觸發)驗證整個 CI 流程跑得通。
|
||||
4. Merge 進 master,觀察 VPS 首次「build-then-up」部署,確認:image 建置成功、五個服務都用新 image 啟動、上傳檔案(`storage/app/public` volume)沒有遺失、既有功能(預約、聊天室、圖片上傳)正常。
|
||||
5. **Rollback 策略**:VPS 上舊的 `cfdive-platform` image(build 前的版本)不會被 `docker compose build` 自動刪除,若新版本異常可 `git revert` 該次 commit 並重新 `docker compose build && up -d` 回到前一版;因為程式碼與 vendor 都烘進 image,rollback 不再需要擔心「主機檔案已改但 vendor 版本不對」的情況(這其實是烘進 image 帶來的額外好處:rollback 更乾淨)。
|
||||
|
||||
## Open Questions
|
||||
|
||||
(本次規劃階段已收斂的問題不再列於此——volume 命名已定名為 `storage-app-public`,見 Decision 3;`compose.override.yml` 版控狀態已查證澄清,見 Decision 2;nginx 程式碼來源與 symlink 策略已定案,見 Decision 4、5。目前無待決問題。)
|
||||
@@ -0,0 +1,27 @@
|
||||
## Why
|
||||
|
||||
目前 `cfdive-platform` image 是空殼——`Dockerfile` 沒有 `COPY . /var/www`,程式碼完全靠 `docker-compose.yml` 對 app、nginx、scheduler、queue-worker、reverb 五個服務的 `./:/var/www` bind mount 從主機借過去。這代表 image 本身不含可執行的程式碼,無法搬到另一台機器獨立運作,也不是可版本化、可回溯的部署單位。P1(log 改 stderr、session 改 Redis)與 `file-storage-s3`(上傳檔案可切到 S3)已經拆掉了「container 必須綁死同一台主機磁碟」的兩個主要理由,現在是把程式碼真正烘進 image、讓 container 變成自足部署單位的時機,也是未來水平擴容的前提。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **Dockerfile 改為 multi-stage**:`app` 階段(`php:8.2-fpm`)統一策略為「先 COPY composer manifests → `composer install --no-scripts` → COPY 全專案 → 補跑 `composer dump-autoload --optimize`(不重複加 `--no-dev`,該旗標已在 install 階段決定 vendor 內容)與 `php artisan package:discover --ansi`(`--no-scripts` 跳過的 composer hook 手動補上)→ 執行 `ln -sfn ../storage/app/public public/storage` 建立符號連結」;新增 `web` 階段(`nginx:alpine`)以 `COPY --from=app /var/www/public /var/www/public` 取得 Laravel 的 `public/` 目錄,並**再次獨立執行**同一條 `ln -sfn` 指令建立自己的符號連結(不依賴 `COPY --from` 隱性帶過去,兩階段各自明確宣告)。新增 `.dockerignore` 排除 `vendor/`、`node_modules/`、`.env`、`storage/app/public/*`,避免機敏資訊或本機殘留檔案被烘進 image——這是本次的正確性要求,不是可選優化。
|
||||
- **docker-compose.yml**:移除 app、nginx、scheduler、queue-worker、reverb 五個服務的 `./:/var/www` bind mount;`nginx` 服務改從 `image: nginx:alpine`(直接拉取)改為本地 build(`target: web`,image 命名 `cfdive-nginx-web`)取得程式碼,不再依賴任何執行期共享機制。**BREAKING**:改 code 不再瞬間生效,必須重新 build image 才能部署新版本。
|
||||
- **上傳檔案(storage)處理方式**:本次變更**不強制**先切換到 S3——決定改用具名 volume `storage-app-public`(已定名),掛載至 `app` 與 `nginx` 服務容器內的絕對路徑 `/var/www/storage/app/public`,讓上傳檔案在 image 重建後仍保留;待日後正式切到 `FILESYSTEM_DISK=s3` 後,此 volume 可再移除(留給後續變更處理,不在本次範圍)。
|
||||
- **`.gitea/workflows/deploy.yml`**:部署流程從「`git reset --hard` 改主機檔案 → `docker compose exec app composer install`」改為「`git reset --hard` → `docker compose build` → `docker compose up -d`」,並成為 migration/`config:cache`/`route:cache`/`view:cache`/`event:cache`/`l5-swagger:generate` 這些「部署一次新版本」動作的**唯一**執行位置。**BREAKING**:部署時間變長(每次都要重建 image),不再有「改 code 幾乎瞬間生效」的能力。
|
||||
- **`.env` 傳遞方式**:拿掉 bind mount 後 `.env` 不能再依賴主機檔案直接出現在容器裡,**只在 `app`、`scheduler`、`queue-worker`、`reverb` 四個 Laravel runtime 服務**改用 YAML list 格式的 `env_file:`(`env_file:\n - .env`)指向主機上的 `.env` 路徑——`env_file:` 是把檔案內容以環境變數形式注入容器程序,**不會**在容器內生成一份實體 `.env` 檔案。`nginx` 服務**不**加 `env_file:`,它不需要讀取任何 `.env` 內容,避免不必要的機敏資訊暴露。
|
||||
- **`docker-entrypoint.sh`**:移除「composer.lock 內容比對後才 composer install」「.env 不存在則從 .env.example 複製 + key:generate」「強制改寫 DB_HOST」三段假設容器內是 bind mount 主機目錄的邏輯;同時**移除 migration/cache clear/swagger 生成**(改由 deploy.yml 統一負責,避免與 deploy.yml 職責重疊、也避免 app/scheduler/queue-worker 三個服務各自啟動時重複跑)與**移除 `storage:link`**(symlink 已在 image build 階段建立,執行期不需要)。entrypoint 最終只保留目錄與權限設定這項啟動必要工作。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `image-baked-deployment`:涵蓋 image 建置時內嵌程式碼與 vendor、compose 移除 bind mount、部署流程改為 build-then-up、`.env` 改用 env_file 傳遞、entrypoint 職責調整後的完整行為規格。
|
||||
|
||||
### Modified Capabilities
|
||||
- `scheduler-container`:現有 requirement「Scheduler 以獨立 container 執行」隱含透過 bind mount 取得程式碼,需更新為透過 image 內建程式碼執行,不再掛 `./:/var/www`。
|
||||
|
||||
## Impact
|
||||
|
||||
- **受影響檔案**:`Dockerfile`(改為 multi-stage:`app` + `web`)、`.dockerignore`(新增)、`docker-compose.yml`、`.gitea/workflows/deploy.yml`、`docker/php/docker-entrypoint.sh`
|
||||
- **受影響服務**:app、nginx(改為本地 build 而非拉取 `nginx:alpine`)、scheduler、queue-worker、reverb(全部五個掛 bind mount 的服務)
|
||||
- **受影響流程**:VPS 部署流程(Gitea Actions self-hosted runner)、本機開發流程(開發者需改用 `docker compose build` 才能讓程式碼變動生效,或另外規劃開發環境仍用 bind mount 的 override 方案)
|
||||
- **不受影響**:`file-storage-s3`(本次不強制切換到 S3,上傳檔案改走具名 volume 過渡)、`compose-cloud-baseline`(healthcheck/開發工具隔離邏輯不變)
|
||||
@@ -0,0 +1,153 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Image build 內嵌完整程式碼與相依套件
|
||||
`Dockerfile` 的 `app` 階段 SHALL 依序執行 `COPY composer.json composer.lock`、`composer install --no-dev --optimize-autoloader --no-scripts`、`COPY . /var/www`,使 build 完成的 `cfdive-platform` image 包含完整可執行程式碼(`app/`、`routes/`、`config/` 等)與 `vendor/`,不依賴任何執行期 bind mount 才能取得程式碼。composer manifests SHALL 先於全專案 COPY,以利用 Docker layer cache。
|
||||
|
||||
#### Scenario: 全新環境不需主機程式碼即可啟動
|
||||
- **WHEN** 在一台全新機器上僅有 `cfdive-platform` image(無主機端 checkout 的程式碼目錄)
|
||||
- **THEN** 以該 image 啟動的 container 可正常執行 `php artisan` 指令與服務既有 API 請求
|
||||
|
||||
#### Scenario: Build 完成後檔案權限正確
|
||||
- **WHEN** `docker build` 執行完成
|
||||
- **THEN** `/var/www/storage` 與 `/var/www/bootstrap/cache` 的擁有者為 `www-data`,且具備 PHP-FPM 寫入所需權限
|
||||
|
||||
#### Scenario: 只修改應用程式碼時 composer 層命中快取
|
||||
- **WHEN** `composer.json`/`composer.lock` 內容未變動,僅應用程式碼變動後重新 `docker build`
|
||||
- **THEN** `composer install` 所在的 layer 命中 Docker build cache,不重新執行
|
||||
|
||||
### Requirement: COPY . 之後補跑 composer 的 post-autoload 腳本
|
||||
`composer.json` 的 `post-autoload-dump` 定義了 `Illuminate\Foundation\ComposerScripts::postAutoloadDump` 與 `php artisan package:discover --ansi`,這兩者依賴 Laravel 應用程式碼已存在才能執行,因此 `composer install` 階段 SHALL 使用 `--no-scripts` 跳過。`Dockerfile` 的 `app` 階段 SHALL 在 `COPY . /var/www` 之後補執行 `composer dump-autoload --optimize`(不重複加 `--no-dev`——該旗標已在 `composer install` 階段決定 vendor 內容,此處只是重新掃描 vendor 與新增的 `app/` 目錄產生 classmap)與 `php artisan package:discover --ansi`,確保 optimized classmap 包含專案自身 `App\` 命名空間下的類別、服務提供者自動發現快取正確產生。
|
||||
|
||||
#### Scenario: Image 內服務提供者自動發現快取存在
|
||||
- **WHEN** `docker build` 執行完成
|
||||
- **THEN** `/var/www/bootstrap/cache/packages.php` 與 `/var/www/bootstrap/cache/services.php` 存在且內容反映 `composer.json` 宣告的套件
|
||||
|
||||
#### Scenario: 專案自身類別可被 optimized autoloader 找到
|
||||
- **WHEN** container 以烘好的 image 啟動並執行任一使用 `App\` 命名空間類別的 artisan 指令
|
||||
- **THEN** 指令正常執行,不因 classmap 缺少專案自身類別而拋出 class not found 錯誤
|
||||
|
||||
### Requirement: public/storage 符號連結於 build 階段建立,app 與 web 兩階段各自獨立建立
|
||||
`Dockerfile` 的 `app` 階段與 `web` 階段 SHALL **各自獨立**在其 `public/` 目錄就緒後執行 `ln -sfn ../storage/app/public /var/www/public/storage`(對應 `config/filesystems.php` 的 `links` 設定;`-n` 避免既有符號連結指向目錄時被誤判為目錄而巢狀建立),不依賴 `COPY --from=app` 隱性傳遞符號連結、也不依賴任何容器執行期指令(如 `php artisan storage:link`)。
|
||||
|
||||
#### Scenario: app 階段內存在符號連結
|
||||
- **WHEN** `docker build` 執行完成
|
||||
- **THEN** `cfdive-platform`(`app` 階段)image 內 `/var/www/public/storage` 是一個指向 `../storage/app/public` 的符號連結,不需要容器啟動後才產生
|
||||
|
||||
#### Scenario: web 階段獨立存在符號連結
|
||||
- **WHEN** `docker build` 執行完成
|
||||
- **THEN** `cfdive-nginx-web`(`web` 階段)image 內 `/var/www/public/storage` 也是一個指向 `../storage/app/public` 的符號連結,此連結由 `web` 階段自身的 `RUN ln -sfn` 指令建立,不依賴 `app` 階段的 build 產物或執行期狀態
|
||||
|
||||
#### Scenario: 重複執行符號連結指令具冪等性
|
||||
- **WHEN** Docker layer cache 失效、`RUN ln -sfn ../storage/app/public /var/www/public/storage` 在同一路徑上重複執行
|
||||
- **THEN** 該指令直接取代既有符號連結,不會在其中巢狀建立新的連結檔案
|
||||
|
||||
### Requirement: nginx 服務程式碼與靜態資源來自獨立 build target,不依賴執行期共享機制
|
||||
`Dockerfile` SHALL 定義第二個 build 階段 `web`(`FROM nginx:alpine`),執行 `COPY --from=app /var/www/public /var/www/public` 取得 `app` 階段建置完成的 `public/` 目錄,並 `COPY docker/nginx/conf.d/ /etc/nginx/conf.d/` 帶入既有 nginx 設定。`docker-compose.yml` 的 `nginx` 服務 SHALL 改為從此 `web` target 本地建置(而非拉取官方 `nginx:alpine` image),不依賴任何執行期 volume 或其他跨容器共享機制取得程式碼。
|
||||
|
||||
#### Scenario: nginx image 內建 public 目錄
|
||||
- **WHEN** 執行 `docker compose build`(`nginx` 服務對應 `web` target)
|
||||
- **THEN** 建置完成的 `nginx` image 內 `/var/www/public` 存在且內容與 `app` image build 時的 `public/` 一致
|
||||
|
||||
#### Scenario: 程式碼變更後 nginx 靜態資源同步更新
|
||||
- **WHEN** 應用程式碼(含 `public/` 下的靜態資源)有變更並重新執行 `docker compose build`
|
||||
- **THEN** `nginx` image 的 `/var/www/public` 內容反映最新的建置結果,不存在因具名 volume 只在首次掛載複製一次而導致的內容過期問題
|
||||
|
||||
#### Scenario: nginx 透過 HTTP 正確服務 public/ 下的靜態檔案
|
||||
- **WHEN** `nginx` container 以 build 完成的 `web` image 啟動,對其發出一個 `public/` 目錄下既有靜態檔案(例如 `favicon.ico` 或前端建置後的靜態資源)的 HTTP 請求
|
||||
- **THEN** nginx 直接回應該檔案內容(`try_files` 命中實體檔案),不需要透過 `app` container 轉發
|
||||
|
||||
#### Scenario: nginx 透過 HTTP 正確服務 public/storage 下的已上傳檔案
|
||||
- **WHEN** `nginx` container 已掛載 `storage-app-public` 具名 volume 且該 volume 內存在一個已上傳檔案,對其發出 `/storage/{檔案路徑}` 的 HTTP 請求
|
||||
- **THEN** nginx 透過 `public/storage` 符號連結解析到 `storage-app-public` volume 內的實際檔案並正確回應,不需要透過 `app` container 轉發
|
||||
|
||||
### Requirement: Build context 排除機敏與非必要檔案
|
||||
`.dockerignore` SHALL 排除 `vendor/`、`node_modules/`、`.env`、`storage/app/public/*` 等路徑,確保 `COPY . /var/www` 不會把本機殘留檔案或機敏資訊(如 `.env` 內的資料庫密碼)複製進 image。
|
||||
|
||||
#### Scenario: .env 不出現在 image 內
|
||||
- **WHEN** 主機 build context 目錄存在 `.env`(含資料庫密碼等機敏資訊)並執行 `docker build`
|
||||
- **THEN** build 完成的 image 內 `/var/www/.env` 不存在
|
||||
|
||||
#### Scenario: 本機 vendor 不覆蓋 build 階段安裝結果
|
||||
- **WHEN** 主機 build context 目錄存在本機已安裝的 `vendor/`(可能與 image 內 PHP 版本或平台不相容)
|
||||
- **THEN** build 完成的 image 內 `vendor/` 內容完全來自 image build 階段的 `composer install`,不受主機 `vendor/` 內容影響
|
||||
|
||||
### Requirement: Production compose 不依賴主機 bind mount 取得程式碼
|
||||
`docker-compose.yml`(production)SHALL 不對 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 任何一個服務定義 `./:/var/www` 這類將整個原始碼目錄掛進容器的 volume。
|
||||
|
||||
#### Scenario: 僅用 production compose 啟動時程式碼來自 image
|
||||
- **WHEN** 執行 `docker compose -f docker-compose.yml up -d`
|
||||
- **THEN** 五個服務容器內的 `/var/www` 內容與 image build 時內嵌的內容一致,即使主機端程式碼目錄被刪除,容器仍正常運作
|
||||
|
||||
### Requirement: 本機開發環境透過 override 疊加 bind mount
|
||||
`compose.override.yml` SHALL 為 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 定義 `./:/var/www` bind mount,供本機開發時「改 code 立即生效」使用;此 override 檔案不影響 production compose 行為。
|
||||
|
||||
#### Scenario: 本機開發改 code 立即生效
|
||||
- **WHEN** 開發者於本機執行 `docker compose up -d`(未指定 `-f`,自動疊加 override)並修改後端 PHP 檔案
|
||||
- **THEN** 容器內看到的檔案內容即時反映主機端變更,不需重新 `docker compose build`
|
||||
|
||||
#### Scenario: VPS 部署不受 override 影響
|
||||
- **WHEN** VPS 執行 `docker compose -f docker-compose.yml up -d --remove-orphans`(不吃 override)
|
||||
- **THEN** 容器程式碼完全來自 image,忽略主機上是否存在 `compose.override.yml`
|
||||
|
||||
### Requirement: 上傳檔案透過具名 volume 在 image 重建後保留
|
||||
`docker-compose.yml` SHALL 定義具名 volume `storage-app-public`,掛載至 `app` 與 `nginx` 服務的 `/var/www/storage/app/public`(絕對路徑)路徑,確保使用者上傳的檔案(課程圖片、教練證照、聊天室圖片)在 image 重新 build 並重啟 container 後不遺失。
|
||||
|
||||
#### Scenario: Image 重建後既有上傳檔案仍可存取
|
||||
- **WHEN** 執行 `docker compose build && docker compose up -d` 重建 image 並重啟 container
|
||||
- **THEN** 重建前已上傳的課程圖片等檔案透過 `nginx` 仍可正常存取,內容與重建前一致
|
||||
|
||||
### Requirement: 部署流程改為 build-then-up,且為部署動作唯一執行位置
|
||||
`.gitea/workflows/deploy.yml` SHALL 在 `git reset --hard origin/master` 之後執行 `docker compose build` 重新建置 image,再執行 `docker compose up -d` 套用新 image,取代原本「改寫主機檔案後於既有容器內執行 `composer install`」的流程。Migration、`config:cache`/`route:cache`/`view:cache`/`event:cache`、`l5-swagger:generate` SHALL 只在 `deploy.yml` 執行、且只在新 container 啟動且健康後才執行,`docker-entrypoint.sh` SHALL 不重複執行這些動作。
|
||||
|
||||
#### Scenario: 部署新版程式碼觸發 image 重建
|
||||
- **WHEN** master 分支有新 commit 推送並觸發部署流程
|
||||
- **THEN** 部署流程執行 `docker compose build`,`app`/`scheduler`/`queue-worker`/`reverb` 四個依賴 `cfdive-platform` image 的服務容器、以及 `nginx`(依賴 `cfdive-nginx-web` image)皆以新 image 重新建立
|
||||
|
||||
#### Scenario: Migration 在新 container 就緒後才執行
|
||||
- **WHEN** `docker compose up -d` 執行完成
|
||||
- **THEN** 部署流程等待 `app` container healthcheck 通過後,才執行 `php artisan migrate --force`
|
||||
|
||||
#### Scenario: Migration 與 cache 不在容器啟動時重複執行
|
||||
- **WHEN** `app`、`scheduler`、`queue-worker` 三個共用同一 entrypoint 的服務各自啟動
|
||||
- **THEN** 沒有任何一個容器在啟動過程中執行 migration、cache 系列 artisan 指令或 `l5-swagger:generate`——這些動作僅由 `deploy.yml` 執行一次
|
||||
|
||||
### Requirement: 環境變數透過 env_file 注入 Laravel runtime 服務,nginx 不注入
|
||||
`docker-compose.yml` 中 `app`、`scheduler`、`queue-worker`、`reverb` 四個實際執行 Laravel/PHP 的服務 SHALL 使用 YAML list 格式的 `env_file:`(即 `env_file:` 後接 `- .env` 列表項,而非 `env_file: .env` 純量寫法)指向主機上的 `.env` 檔案路徑,將其內容以環境變數形式注入容器程序,不再透過 bind mount 整個目錄的方式讓容器讀到 `.env`。`nginx` 服務 SHALL **不**設定 `env_file:`——它不需要讀取任何 `.env` 內容,給予存取權屬於不必要的機敏資訊暴露。`env_file:` SHALL 只負責注入環境變數,容器內的檔案系統不會因此產生一份實體 `.env` 檔案。
|
||||
|
||||
#### Scenario: 更新 .env 後不需重建 image 即可套用
|
||||
- **WHEN** 操作者修改主機上的 `.env` 內容並執行 `docker compose up -d`(不重新 build)
|
||||
- **THEN** `app`、`scheduler`、`queue-worker`、`reverb` 四個服務容器重新建立並讀取到更新後的環境變數
|
||||
|
||||
#### Scenario: 容器內不存在實體 .env 檔案
|
||||
- **WHEN** container 以 `env_file:` 注入環境變數的方式啟動
|
||||
- **THEN** 容器內 `/var/www/.env` 檔案不存在,程式讀取設定值皆透過環境變數(`env()`/`getenv()`)取得
|
||||
|
||||
#### Scenario: nginx 容器不具備 .env 內容存取權
|
||||
- **WHEN** `nginx` container 啟動
|
||||
- **THEN** 該容器的環境變數中不存在 `.env` 檔案內定義的變數(如 `DB_PASSWORD`、`APP_KEY`),因為 `nginx` 服務未設定 `env_file:`
|
||||
|
||||
### Requirement: Entrypoint 移除主機目錄假設邏輯
|
||||
`docker-entrypoint.sh` SHALL 不再包含「`.env` 不存在時從 `.env.example` 複製並執行 `key:generate`」「依 `composer.lock` 內容比對決定是否重新 `composer install`」「強制以 `preg_replace` 改寫 `.env` 內 `DB_HOST` 值」這三段邏輯,因為 image 已在 build 階段完成 vendor 安裝、環境變數已由 `env_file:` 於執行期注入。
|
||||
|
||||
#### Scenario: Container 啟動不再執行執行期 composer install
|
||||
- **WHEN** container 以烘好程式碼的 image 啟動
|
||||
- **THEN** entrypoint 不執行任何 `composer install` 指令,`vendor/` 內容與 image build 時完全一致
|
||||
|
||||
#### Scenario: Container 啟動不再自動產生 .env
|
||||
- **WHEN** container 啟動且未透過 `env_file:` 提供有效環境變數
|
||||
- **THEN** entrypoint 不自動複製 `.env.example` 或執行 `php artisan key:generate`,由部署流程確保 `.env` 存在為前提條件
|
||||
|
||||
#### Scenario: Container 啟動不再改寫 .env 檔案內容
|
||||
- **WHEN** container 啟動
|
||||
- **THEN** entrypoint 不執行任何修改 `.env` 檔案內容的指令,`DB_HOST` 等設定值一律由 `docker-compose.yml` 的 `environment:` 或 `.env` 提供
|
||||
|
||||
### Requirement: Entrypoint 只保留啟動必要工作
|
||||
`docker-entrypoint.sh` SHALL 只保留目錄與權限設定(`storage`、`bootstrap/cache`),不再包含等待 MySQL 的背景迴圈、migration、cache clear 系列指令、`l5-swagger:generate`、`storage:link`(symlink 已於 image build 階段建立,見「public/storage 符號連結於 build 階段建立」requirement)。
|
||||
|
||||
#### Scenario: Entrypoint 不等待 MySQL
|
||||
- **WHEN** container 啟動且 MySQL 尚未就緒
|
||||
- **THEN** entrypoint 不因此阻塞或執行任何等待迴圈,php-fpm 正常啟動
|
||||
|
||||
#### Scenario: Entrypoint 不執行 storage:link
|
||||
- **WHEN** container 啟動且 `storage-app-public` 具名 volume 已掛載
|
||||
- **THEN** entrypoint 不執行任何 `storage:link` 相關指令,`/var/www/public/storage` 符號連結已存在於 image 內容中
|
||||
@@ -0,0 +1,16 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Scheduler 以獨立 container 執行
|
||||
`docker-compose.yml` SHALL 定義 `scheduler` service,使用 `cfdive-platform` image(其中已於 build 階段內嵌完整程式碼,不依賴 `./:/var/www` bind mount),以 `php artisan schedule:work` 前台輪詢方式執行 Laravel Scheduler,取代原本在 app container 內的 cron daemon。
|
||||
|
||||
#### Scenario: Scheduler container 隨 app 啟動
|
||||
- **WHEN** 執行 `docker compose up -d` 且 `app` container 已 healthy
|
||||
- **THEN** `scheduler` container 啟動並持續執行 `php artisan schedule:work`
|
||||
|
||||
#### Scenario: App container 重啟時 scheduler 等待就緒
|
||||
- **WHEN** `app` container 重啟中(unhealthy)
|
||||
- **THEN** `scheduler` container 依 `depends_on` 等待 `app` healthy 後才啟動,避免 scheduler 在 PHP-FPM 未就緒時執行任務
|
||||
|
||||
#### Scenario: Scheduler 不依賴主機 bind mount 取得程式碼
|
||||
- **WHEN** 執行 `docker compose -f docker-compose.yml up -d`(production,不含 override)
|
||||
- **THEN** `scheduler` container 執行的排程任務程式碼完全來自 `cfdive-platform` image build 時內嵌的內容,與主機端程式碼目錄是否存在無關
|
||||
@@ -0,0 +1,65 @@
|
||||
## 1. Dockerfile 改為 multi-stage:app 階段內嵌程式碼與相依套件
|
||||
|
||||
- [ ] 1.1 [後端] `Dockerfile`:第一階段命名為 `FROM php:8.2-fpm AS app`(既有內容套用此階段名稱,供後續 `web` 階段 `COPY --from=app` 使用)
|
||||
- [ ] 1.2 [後端] `Dockerfile`:在 `docker-php-ext-install` 之後、`COPY docker/php/local.ini` 之前,先 `COPY composer.json composer.lock /var/www/` 並執行 `composer install --no-dev --optimize-autoloader --no-interaction --no-scripts`(利用 layer cache,composer 檔案未變動時不重跑;`--no-scripts` 是必要的,因為此時 `app/`、`artisan` 尚未存在,composer 的 `post-autoload-dump` hook 會失敗)
|
||||
- [ ] 1.3 [後端] `Dockerfile`:新增 `COPY . /var/www`(放在 composer install 之後),並在其後重新執行 `chown -R www-data:www-data /var/www` 與 `chmod -R 775 storage bootstrap/cache`(COPY 進來的檔案預設 owner 是 root)
|
||||
- [ ] 1.4 [後端] `Dockerfile`:`COPY . /var/www` 之後補執行 `composer dump-autoload --optimize`(不重複加 `--no-dev`,該旗標已由前面的 `composer install --no-dev` 決定 vendor 內容,這裡只是重新掃描產生 classmap)與 `php artisan package:discover --ansi`(補上 `--no-scripts` 跳過的服務提供者自動發現,確認 `bootstrap/cache/packages.php`/`services.php` 正確產生)
|
||||
- [ ] 1.5 [後端] `Dockerfile`:`COPY . /var/www` 之後執行 `RUN ln -sfn ../storage/app/public /var/www/public/storage`(`-n` 避免既有符號連結指向目錄時被誤判巢狀建立),在 build 階段建立 `public/storage` 符號連結(對應 `config/filesystems.php` 的 `links` 設定),不依賴任何執行期 artisan 指令
|
||||
- [ ] 1.6 [後端] 新增 `.dockerignore`(目前不存在,屬本次必要項目非優化):排除 `vendor/`、`node_modules/`、`.env`、`storage/app/public/*`,避免機敏資訊(`.env` 內資料庫密碼)或本機殘留檔案被 `COPY . /var/www` 複製進 image
|
||||
|
||||
## 2. Dockerfile 新增 web 階段(nginx 程式碼來源)
|
||||
|
||||
- [ ] 2.1 [後端] `Dockerfile`:新增第二階段 `FROM nginx:alpine AS web`
|
||||
- [ ] 2.2 [後端] `Dockerfile`:`web` 階段執行 `COPY --from=app /var/www/public /var/www/public`,取得 `app` 階段建置完成的 `public/` 目錄
|
||||
- [ ] 2.3 [後端] `Dockerfile`:`web` 階段**再次獨立執行** `RUN ln -sfn ../storage/app/public /var/www/public/storage`(不依賴 2.2 的 `COPY --from` 隱性帶入 1.5 建立的符號連結——兩階段各自明確宣告,避免日後若複製方式調整導致符號連結無聲消失)
|
||||
- [ ] 2.4 [後端] `Dockerfile`:`web` 階段執行 `COPY docker/nginx/conf.d/ /etc/nginx/conf.d/`,帶入既有 nginx 設定(`docker/nginx/conf.d/app.conf` 的 `root /var/www/public;` 設定不需更動,`SCRIPT_FILENAME` 路徑在 app/nginx 兩個容器間仍一致)
|
||||
|
||||
## 3. docker-compose.yml 調整(production)
|
||||
|
||||
- [ ] 3.1 [後端] `docker-compose.yml`:移除 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 五個服務的 `./:/var/www` volume 定義
|
||||
- [ ] 3.2 [後端] `docker-compose.yml`:`nginx` 服務的 `image: nginx:alpine` 改為 `build: {context: ., dockerfile: Dockerfile, target: web}`,並將 `image:` 欄位改為本地命名(如 `cfdive-nginx-web`,與既有 Vue/Vite 前端的 `cfdive-frontend` 區分)
|
||||
- [ ] 3.3 [後端] `docker-compose.yml`:新增具名 volume `storage-app-public`(定名,見 design.md Decision 3),掛載至 `app` 與 `nginx` 服務容器內的 **`/var/www/storage/app/public`**(絕對路徑),並在底部 `volumes:` 區塊註冊
|
||||
- [ ] 3.4 [後端] `docker-compose.yml`:**只在** `app`、`scheduler`、`queue-worker`、`reverb` **四個** Laravel runtime 服務加上 YAML list 格式的 `env_file:`:
|
||||
```yaml
|
||||
env_file:
|
||||
- .env
|
||||
```
|
||||
(不用 `env_file: .env` 純量寫法;`nginx` 服務**不**加 `env_file:`,它不需要讀取 `.env` 內容,避免不必要的機敏資訊暴露);移除 `environment:` 區塊裡與 `.env` 重複的 `DB_*` 硬編碼項目(改由 `.env` 統一提供,避免兩處來源衝突)
|
||||
|
||||
## 4. 本機開發環境 override
|
||||
|
||||
- [ ] 4.1 [後端] 更新既有已版控的 `compose.override.yml`(`git ls-files` 確認已追蹤,非未版控檔案):為 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 補上 `./:/var/www` bind mount,維持本機開發改 code 立即生效;不需另建 `.example` 檔案,此檔案本身即為所有開發者共用的標準範本
|
||||
|
||||
## 5. entrypoint 職責調整(只保留啟動必要工作,migration/cache/swagger/storage:link 全部移除)
|
||||
|
||||
- [ ] 5.1 [後端] `docker/php/docker-entrypoint.sh`:移除「`.env` 不存在則複製 `.env.example` + `key:generate`」區塊
|
||||
- [ ] 5.2 [後端] `docker/php/docker-entrypoint.sh`:移除「依 `composer.lock` 內容比對決定是否重新 `composer install`」區塊
|
||||
- [ ] 5.3 [後端] `docker/php/docker-entrypoint.sh`:移除強制改寫 `DB_HOST=db` 的 `preg_replace` 邏輯(改由 `.env`/compose `environment:` 直接提供正確值)
|
||||
- [ ] 5.4 [後端] `docker/php/docker-entrypoint.sh`:移除等待 MySQL 的背景迴圈,以及該迴圈之後的 migration、`config:clear`/`cache:clear`/`route:clear`/`view:clear`、`l5-swagger:generate` 整段背景執行邏輯(責任移交 deploy.yml,見 group 6)
|
||||
- [ ] 5.5 [後端] `docker/php/docker-entrypoint.sh`:移除 `storage:link` 執行(symlink 已於 1.5 在 image build 階段建立,執行期不需要)
|
||||
- [ ] 5.6 [後端] `docker/php/docker-entrypoint.sh`:確認保留目錄與權限設定(`storage`、`bootstrap/cache`)作為 entrypoint 最終僅存的啟動必要工作
|
||||
|
||||
## 6. 部署流程(.gitea/workflows/deploy.yml)
|
||||
|
||||
- [ ] 6.1 [後端] `deploy.yml`:`git reset --hard origin/master` 之後新增 `docker compose build` step
|
||||
- [ ] 6.2 [後端] `deploy.yml`:把原本「Install Composer dependencies」step 移除(已在 image build 階段完成)
|
||||
- [ ] 6.3 [後端] `deploy.yml`:新增 `docker compose up -d --remove-orphans` step,並加入等待 `app` container healthy 的檢查(如輪詢 `docker compose ps --format json` 或既有 healthcheck 狀態)後才繼續
|
||||
- [ ] 6.4 [後端] `deploy.yml`:確認 migration、`config:cache`/`route:cache`/`view:cache`/`event:cache`、`l5-swagger:generate` 幾個 step 移到 `app` healthy 之後執行
|
||||
- [ ] 6.5 [後端] `deploy.yml`:移除原本獨立的「Restart queue worker」step(`docker compose up -d` 已會因 image 變更重建該 container)
|
||||
|
||||
## 7. 驗證
|
||||
|
||||
- [ ] 7.1 [整合測試] 本機執行 `docker compose build && docker compose up -d`(含 override),確認五個服務容器全部啟動、`app` healthcheck(`nc -z 127.0.0.1 9000`)通過
|
||||
- [ ] 7.2 [整合測試] 本機容器內執行 `php artisan test`,確認既有測試套件全綠(比對變更前的 baseline 通過數)
|
||||
- [ ] 7.3 [整合測試] 驗證本機開發體驗:override 生效時修改一個 PHP 檔案,確認容器內立即反映變更(不需 rebuild)
|
||||
- [ ] 7.4 [整合測試] 驗證 production 模式:`docker compose -f docker-compose.yml up -d`(不含 override)啟動後,暫時搬移或改名主機端專案目錄,確認容器仍正常回應 API 請求
|
||||
- [ ] 7.5 [整合測試] 驗證上傳檔案持久性:上傳一張課程圖片,執行 `docker compose build && docker compose up -d` 重建 image,確認該圖片透過 nginx 仍可正常存取
|
||||
- [ ] 7.6 [整合測試] 驗證 `.env` 熱更新:修改 `.env` 內一個非敏感值,`docker compose up -d`(不 build),確認容器內 `php artisan tinker` 讀到新值
|
||||
- [ ] 7.7 [整合測試] 在 Gitea Actions 上以手動觸發或測試分支驗證完整 `deploy.yml` 新流程跑得通,確認 build 失敗時舊 container 是否仍維持運作(design.md 已知風險項)
|
||||
- [ ] 7.8 [整合測試] VPS 正式部署後,人工驗證預約、聊天室、課程圖片上傳、教練證照上傳等現有功能正常,並確認 `scheduler`/`queue-worker`/`reverb` 皆以新 image 啟動(`docker compose ps` 確認 image ID 一致)
|
||||
- [ ] 7.9 [整合測試] 確認 entrypoint/deploy 職責切分無重疊:`docker compose up -d` 啟動 `app`/`scheduler`/`queue-worker` 三個共用 entrypoint 的服務時,觀察其 log 不應出現 migration 或 `l5-swagger:generate` 執行紀錄,這些只應出現在 `deploy.yml` 執行紀錄裡
|
||||
- [ ] 7.10 [整合測試] 驗證 nginx 靜態資源獨立於 app:`docker compose build` 後單獨檢查 `nginx` container 內 `/var/www/public` 存在且可直接存取(如 `docker compose exec nginx ls /var/www/public`),停掉 `app` container 後 nginx 仍能透過 HTTP 直接回應 `public/` 下的靜態檔案(如 `favicon.ico`),確認不依賴 `app` container 是否存在或執行中
|
||||
- [ ] 7.11 [整合測試] 驗證 nginx 的 public/storage 符號連結獨立建立:`docker compose exec nginx ls -la /var/www/public/storage` 確認符號連結存在且指向 `../storage/app/public`,比對 `docker compose exec app ls -la /var/www/public/storage` 確認兩個容器**各自獨立**擁有此符號連結(非透過同一個共享狀態)
|
||||
- [ ] 7.12 [整合測試] 驗證 nginx 透過 public/storage 正確服務已上傳檔案:先透過應用上傳一張課程圖片,直接對 nginx 發出該檔案的 HTTP 請求(`/storage/{path}`),確認回應內容正確,且此請求全程未經過 `app` container(php-fpm 只處理 `.php` 副檔名請求,見 `docker/nginx/conf.d/app.conf` 的 `location ~ \.php$`)
|
||||
- [ ] 7.13 [整合測試] 驗證 composer 自動發現正確:`docker compose exec app php artisan route:list` 或任一依賴自動發現的第三方套件功能(如 l5-swagger)正常運作,確認 `package:discover` 於 build 階段正確執行
|
||||
- [ ] 7.14 [整合測試] 驗證 nginx 容器不具備 `.env` 內容:`docker compose exec nginx env | grep -i "DB_PASSWORD\|APP_KEY"` 應無任何輸出,確認 `nginx` 服務未設定 `env_file:` 生效
|
||||
Reference in New Issue
Block a user