05a116beee
Run Tests / test (pull_request) Successful in 32s
VPS 部署與功能驗證皆已完成(tasks.md 7.7/7.8),change 移入 archive/2026-08-04-bake-image-remove-bind-mount。同步兩份 delta spec: 新建 image-baked-deployment 主規格(multi-stage build、symlink 建立、 nginx 獨立 build target、env_file 注入等 11 條 requirement);更新 scheduler-container 補上「不依賴 bind mount」的敘述與 scenario。 openspec validate --strict --specs 39/39 全過。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0142LC1kQb8EyV59TjHDkhWS
12 KiB
12 KiB
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-dumphook 會失敗) - 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/wwwvolume 定義 - 3.2 [後端]
docker-compose.yml:nginx服務的image: nginx:alpine改為build: {context: ., dockerfile: Dockerfile, target: web},並將image:欄位改為本地命名(如cfdive-nginx-web,與既有 Vue/Vite 前端的cfdive-frontend區分);同時app服務的build:也必須明確加上target: app(實作驗證時發現:省略會導致 Docker 預設建到最後一個 stage,即 nginx,讓cfdive-platform實際變成 nginx image) - 3.3 [後端]
docker-compose.yml:新增具名 volumestorage-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::(不用env_file: - .envenv_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/wwwbind 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/composeenvironment:直接提供正確值) - 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 buildstep - 6.2 [後端]
deploy.yml:把原本「Install Composer dependencies」step 移除(已在 image build 階段完成) - 6.3 [後端]
deploy.yml:新增docker compose up -d --remove-orphansstep,並加入等待appcontainer healthy 的檢查(如輪詢docker compose ps --format json或既有 healthcheck 狀態)後才繼續 - 6.4 [後端]
deploy.yml:確認 migration、config:cache/route:cache/view:cache/event:cache、l5-swagger:generate幾個 step 移到apphealthy 之後執行 - 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),確認五個服務容器全部啟動、apphealthcheck(nc -z 127.0.0.1 9000)通過 - 7.2 [整合測試] 本機容器內執行
php artisan test,確認既有測試套件全綠(比對變更前的 baseline 通過數)——239 passed/578 assertions(dev override 下 vendor 含 dev 依賴);發現並修復env_file:導致.env全部變數(非僅 DB_*)洩漏進容器成真實環境變數、覆蓋phpunit.xml測試設定值的問題,詳見下方「實作期間發現並修復」 - 7.3 [整合測試] 驗證本機開發體驗:override 生效時修改一個 PHP 檔案,確認容器內立即反映變更(不需 rebuild)——修改
tests/bootstrap.php/phpunit.xml後未 rebuild 即生效,驗證通過 - 7.4 [整合測試] 驗證 production 模式:
docker compose -f docker-compose.yml up -d(不含 override)啟動後確認容器仍正常回應 API 請求——首頁 200、/api/diving-offers回傳正確 JSON、migration/cache/swagger 全部成功 - 7.5 [整合測試] 驗證上傳檔案持久性:建立測試檔案於
storage/app/public/,docker compose up -d --force-recreate app nginx重建容器,確認該檔案透過 nginx/storage/...仍可正常存取 - 7.6 [整合測試] 驗證
.env熱更新:修改.env內APP_NAME,docker compose up -d(不 build)重建 container,確認容器內env('APP_NAME')讀到新值,測試後已還原 - 7.7 [整合測試] 在 Gitea Actions 上以手動觸發或測試分支驗證完整
deploy.yml新流程跑得通,確認 build 失敗時舊 container 是否仍維持運作(design.md 已知風險項)——Hank 確認 VPS 上 Gitea Actions 執行正常 - 7.8 [整合測試] VPS 正式部署後,人工驗證預約、聊天室、課程圖片上傳、教練證照上傳等現有功能正常,並確認
scheduler/queue-worker/reverb皆以新 image 啟動(docker compose ps確認 image ID 一致)——Hank 確認 VPS 功能正常;docker compose ps顯示 app/scheduler/queue-worker/reverb 皆為cfdive-platformimage、同批次(16 分鐘前)一起重建,nginx 為cfdive-nginx-web亦同批次重建,app healthcheck healthy - 7.9 [整合測試] 確認 entrypoint/deploy 職責切分無重疊:
docker compose up -d啟動app/scheduler/queue-worker三個共用 entrypoint 的服務時,觀察其 log 不應出現 migration 或l5-swagger:generate執行紀錄,這些只應出現在deploy.yml執行紀錄裡——三個容器 log 都只有初始化與 php-fpm 啟動訊息 - 7.10 [整合測試] 驗證 nginx 靜態資源獨立於 app:停掉
appcontainer 後 nginx 仍能透過 HTTP 直接回應public/下的靜態檔案(favicon.ico200)與 production 模式下/回應 200,確認不依賴appcontainer 是否存在或執行中 - 7.11 [整合測試] 驗證 nginx 的 public/storage 符號連結獨立建立:
docker run --rm cfdive-platform/cfdive-nginx-web直接對 image(非透過 dev override 容器)執行readlink /var/www/public/storage,兩者皆為../storage/app/public,各自獨立存在於各自 image 內 - 7.12 [整合測試] 驗證 nginx 透過 public/storage 正確服務已上傳檔案:停掉
appcontainer 後直接對 nginx 發出/storage/test2/file.txt請求,回應內容正確(200),確認全程未經過appcontainer - 7.13 [整合測試] 驗證 composer 自動發現正確:
docker compose exec app php artisan route:list正常回傳完整路由清單,l5-swagger:generate正常執行,確認package:discover於 build 階段正確執行 - 7.14 [整合測試] 驗證 nginx 容器不具備
.env內容:docker compose exec nginx env | grep -i "DB_PASSWORD\|APP_KEY"無任何輸出,確認nginx服務未設定env_file:生效
實作期間發現並修復(2026-08-04,不在原規劃內)
docker-compose.yml的app服務缺少target: app:Dockerfile改 multi-stage 後,build:若未指定target:會預設建到最後一個 stage(web/nginx),導致cfdive-platformimage 實際上是 nginx 而非 php-fpm(docker image inspect驗證 entrypoint 為nginx -g daemon off;)。已在app服務的build:補上target: app並重新驗證兩個 image 的 entrypoint 分別正確。.dockerignore需排除public/storage:本機既有的public/storage符號連結(先前用php artisan storage:link建立,絕對路徑指向/var/www/storage/app/public)在 Windows 上會讓 Docker build context 傳輸失敗(archive/tar: unknown file mode)。已加入.dockerignore。env_file:使.env全部變數成為真實環境變數,覆蓋測試隔離:tests/bootstrap.php/phpunit.xml先前只强制覆蓋DB_CONNECTION/DB_DATABASE(因為之前只有這兩個透過 composeenvironment:洩漏),改用env_file: .env後.env的APP_ENV=local/CACHE_STORE=redis/SESSION_DRIVER=database/QUEUE_CONNECTION=database/MAIL_MAILER=smtp/BCRYPT_ROUNDS=12等全部變成真實環境變數,覆蓋phpunit.xml的 testing 值,導致 9 個測試失敗(快取污染、外寄信、500 錯誤等)。已擴大tests/bootstrap.php強制覆蓋清單涵蓋phpunit.xml宣告的全部 11 個變數,並在phpunit.xml對應加上force="true"自我說明。修復後 239 tests 全綠。