Files
CFDivePlatform/openspec/changes/bake-image-remove-bind-mount/tasks.md
T
a620906209 6a7d98b0fe
Run Tests / test (pull_request) Successful in 32s
feat(docker): 移除 bind mount,程式碼與 vendor 烘進 image
Dockerfile 改 multi-stage(app + web),app/scheduler/queue-worker/
reverb/nginx 五個服務都不再依賴 ./:/var/www bind mount 取得程式碼;
nginx 改本地 build 從 app 階段 COPY --from 取得 public/,public/storage
symlink 在兩階段各自於 build 時建立。docker-compose.yml 新增具名 volume
storage-app-public 讓上傳檔案跨 image 重建保留;env_file: 取代原本
bind mount 帶入 .env 的方式,只給四個 Laravel runtime 服務。
compose.override.yml 補上開發用 bind mount 維持改 code 立即生效;
docker-entrypoint.sh 收斂到只剩目錄權限設定,migration/cache/swagger
移到 deploy.yml 統一負責,避免職責重疊。

實作驗證時發現並修復三個規劃階段沒預見的問題:app 服務的 build: 缺
target: app 導致預設建到最後一個 stage(image 實際變成 nginx);
.dockerignore 需排除本機既有的 public/storage 符號連結(Windows 上
會讓 build context 打包失敗);env_file: 讓 .env 全部變數變成真實
環境變數、蓋掉 phpunit.xml 的測試隔離設定,已擴大 tests/bootstrap.php
的強制覆蓋清單修復(239 tests 全綠)。

本機 dev 模式與 production 模式(無 override)皆完整驗證:healthcheck、
migration/cache/swagger、上傳檔案持久性、nginx 獨立於 app 服務靜態檔
與 storage 檔案、.env 熱更新、nginx 無 .env 存取權。

openspec change: bake-image-remove-bind-mount

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0142LC1kQb8EyV59TjHDkhWS
2026-08-04 16:59:18 +08:00

12 KiB
Raw Blame History

1. Dockerfile 改為 multi-stageapp 階段內嵌程式碼與相依套件

  • 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 cachecomposer 檔案未變動時不重跑;--no-scripts 是必要的,因為此時 app/artisan 尚未存在,composer 的 post-autoload-dump hook 會失敗)
  • 1.3 [後端] Dockerfile:新增 COPY . /var/www(放在 composer install 之後),並在其後重新執行 chown -R www-data:www-data /var/wwwchmod -R 775 storage bootstrap/cacheCOPY 進來的檔案預設 owner 是 root)
  • 1.4 [後端] DockerfileCOPY . /var/www 之後補執行 composer dump-autoload --optimize(不重複加 --no-dev,該旗標已由前面的 composer install --no-dev 決定 vendor 內容,這裡只是重新掃描產生 classmap)與 php artisan package:discover --ansi(補上 --no-scripts 跳過的服務提供者自動發現,確認 bootstrap/cache/packages.phpservices.php 正確產生)
  • 1.5 [後端] DockerfileCOPY . /var/www 之後執行 RUN ln -sfn ../storage/app/public /var/www/public/storage-n 避免既有符號連結指向目錄時被誤判巢狀建立),在 build 階段建立 public/storage 符號連結(對應 config/filesystems.phplinks 設定),不依賴任何執行期 artisan 指令
  • 1.6 [後端] 新增 .dockerignore(目前不存在,屬本次必要項目非優化):排除 vendor/node_modules/.envstorage/app/public/*,避免機敏資訊(.env 內資料庫密碼)或本機殘留檔案被 COPY . /var/www 複製進 image

2. Dockerfile 新增 web 階段(nginx 程式碼來源)

  • 2.1 [後端] Dockerfile:新增第二階段 FROM nginx:alpine AS web
  • 2.2 [後端] Dockerfileweb 階段執行 COPY --from=app /var/www/public /var/www/public,取得 app 階段建置完成的 public/ 目錄
  • 2.3 [後端] Dockerfileweb 階段再次獨立執行 RUN ln -sfn ../storage/app/public /var/www/public/storage(不依賴 2.2 的 COPY --from 隱性帶入 1.5 建立的符號連結——兩階段各自明確宣告,避免日後若複製方式調整導致符號連結無聲消失)
  • 2.4 [後端] Dockerfileweb 階段執行 COPY docker/nginx/conf.d/ /etc/nginx/conf.d/,帶入既有 nginx 設定(docker/nginx/conf.d/app.confroot /var/www/public; 設定不需更動,SCRIPT_FILENAME 路徑在 app/nginx 兩個容器間仍一致)

3. docker-compose.yml 調整(production

  • 3.1 [後端] docker-compose.yml:移除 appnginxschedulerqueue-workerreverb 五個服務的 ./:/var/www volume 定義
  • 3.2 [後端] docker-compose.ymlnginx 服務的 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:新增具名 volume storage-app-public(定名,見 design.md Decision 3),掛載至 appnginx 服務容器內的 /var/www/storage/app/public(絕對路徑),並在底部 volumes: 區塊註冊
  • 3.4 [後端] docker-compose.yml只在 appschedulerqueue-workerreverb 四個 Laravel runtime 服務加上 YAML list 格式的 env_file:
    env_file:
      - .env
    
    (不用 env_file: .env 純量寫法;nginx 服務env_file:,它不需要讀取 .env 內容,避免不必要的機敏資訊暴露);移除 environment: 區塊裡與 .env 重複的 DB_* 硬編碼項目(改由 .env 統一提供,避免兩處來源衝突)

4. 本機開發環境 override

  • 4.1 [後端] 更新既有已版控的 compose.override.ymlgit ls-files 確認已追蹤,非未版控檔案):為 appnginxschedulerqueue-workerreverb 補上 ./:/var/www bind mount,維持本機開發改 code 立即生效;不需另建 .example 檔案,此檔案本身即為所有開發者共用的標準範本
  • 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=dbpreg_replace 邏輯(改由 .envcompose environment: 直接提供正確值)
  • 5.4 [後端] docker/php/docker-entrypoint.sh:移除等待 MySQL 的背景迴圈,以及該迴圈之後的 migration、config:clear/cache:clear/route:clear/view:clearl5-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:確認保留目錄與權限設定(storagebootstrap/cache)作為 entrypoint 最終僅存的啟動必要工作

6. 部署流程(.gitea/workflows/deploy.yml

  • 6.1 [後端] deploy.ymlgit 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:cachel5-swagger:generate 幾個 step 移到 app healthy 之後執行
  • 6.5 [後端] deploy.yml:移除原本獨立的「Restart queue worker」stepdocker compose up -d 已會因 image 變更重建該 container

7. 驗證

  • 7.1 [整合測試] 本機執行 docker compose build && docker compose up -d(含 override),確認五個服務容器全部啟動、app healthchecknc -z 127.0.0.1 9000)通過
  • 7.2 [整合測試] 本機容器內執行 php artisan test,確認既有測試套件全綠(比對變更前的 baseline 通過數)——239 passed/578 assertionsdev override 下 vendor 含 dev 依賴);發現並修復 env_file: 導致 .env 全部變數(非僅 DB_*)洩漏進容器成真實環境變數、覆蓋 phpunit.xml 測試設定值的問題,詳見下方「實作期間發現並修復」
  • 7.3 [整合測試] 驗證本機開發體驗:override 生效時修改一個 PHP 檔案,確認容器內立即反映變更(不需 rebuild)——修改 tests/bootstrap.phpphpunit.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 熱更新:修改 .envAPP_NAMEdocker compose up -d(不 build)重建 container,確認容器內 env('APP_NAME') 讀到新值,測試後已還原
  • 7.7 [整合測試] 在 Gitea Actions 上以手動觸發或測試分支驗證完整 deploy.yml 新流程跑得通,確認 build 失敗時舊 container 是否仍維持運作(design.md 已知風險項)——需 VPS/CI 環境,留給 Hank 執行
  • 7.8 [整合測試] VPS 正式部署後,人工驗證預約、聊天室、課程圖片上傳、教練證照上傳等現有功能正常,並確認 scheduler/queue-worker/reverb 皆以新 image 啟動(docker compose ps 確認 image ID 一致)——需 VPS 環境,留給 Hank 執行
  • 7.9 [整合測試] 確認 entrypointdeploy 職責切分無重疊:docker compose up -d 啟動 app/scheduler/queue-worker 三個共用 entrypoint 的服務時,觀察其 log 不應出現 migration 或 l5-swagger:generate 執行紀錄,這些只應出現在 deploy.yml 執行紀錄裡——三個容器 log 都只有初始化與 php-fpm 啟動訊息
  • 7.10 [整合測試] 驗證 nginx 靜態資源獨立於 app:停掉 app container 後 nginx 仍能透過 HTTP 直接回應 public/ 下的靜態檔案(favicon.ico 200)與 production 模式下 / 回應 200,確認不依賴 app container 是否存在或執行中
  • 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 正確服務已上傳檔案:停掉 app container 後直接對 nginx 發出 /storage/test2/file.txt 請求,回應內容正確(200),確認全程未經過 app container
  • 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.ymlapp 服務缺少 target: appDockerfile 改 multi-stage 後,build: 若未指定 target: 會預設建到最後一個 stageweb/nginx),導致 cfdive-platform image 實際上是 nginx 而非 php-fpmdocker 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.phpphpunit.xml 先前只强制覆蓋 DB_CONNECTIONDB_DATABASE(因為之前只有這兩個透過 compose environment: 洩漏),改用 env_file: .env.envAPP_ENV=localCACHE_STORE=redisSESSION_DRIVER=databaseQUEUE_CONNECTION=databaseMAIL_MAILER=smtpBCRYPT_ROUNDS=12 等全部變成真實環境變數,覆蓋 phpunit.xml 的 testing 值,導致 9 個測試失敗(快取污染、外寄信、500 錯誤等)。已擴大 tests/bootstrap.php 強制覆蓋清單涵蓋 phpunit.xml 宣告的全部 11 個變數,並在 phpunit.xml 對應加上 force="true" 自我說明。修復後 239 tests 全綠。