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
15 KiB
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-platformimage(無主機端 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服務對應webtarget) - THEN 建置完成的
nginximage 內/var/www/public存在且內容與appimage build 時的public/一致
Scenario: 程式碼變更後 nginx 靜態資源同步更新
- WHEN 應用程式碼(含
public/下的靜態資源)有變更並重新執行docker compose build - THEN
nginximage 的/var/www/public內容反映最新的建置結果,不存在因具名 volume 只在首次掛載複製一次而導致的內容過期問題
Scenario: nginx 透過 HTTP 正確服務 public/ 下的靜態檔案
- WHEN
nginxcontainer 以 build 完成的webimage 啟動,對其發出一個public/目錄下既有靜態檔案(例如favicon.ico或前端建置後的靜態資源)的 HTTP 請求 - THEN nginx 直接回應該檔案內容(
try_files命中實體檔案),不需要透過appcontainer 轉發
Scenario: nginx 透過 HTTP 正確服務 public/storage 下的已上傳檔案
- WHEN
nginxcontainer 已掛載storage-app-public具名 volume 且該 volume 內存在一個已上傳檔案,對其發出/storage/{檔案路徑}的 HTTP 請求 - THEN nginx 透過
public/storage符號連結解析到storage-app-publicvolume 內的實際檔案並正確回應,不需要透過appcontainer 轉發
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: 每個 compose service 明確指定 multi-stage build target
docker-compose.yml 中依賴這份 multi-stage Dockerfile 建置的每一個服務(app 與 nginx)SHALL 在其 build: 設定明確指定 target:(app 服務指定 target: app,nginx 服務指定 target: web),不得省略。
Scenario: app 服務建置出的是 php-fpm image 而非 nginx
- WHEN 執行
docker compose build app - THEN 建置完成的
cfdive-platformimage 其 entrypoint/預設指令為啟動 php-fpm,而非 nginx(docker image inspect cfdive-platform確認Config.Cmd為["php-fpm"])
Scenario: 省略 target 會建到 Dockerfile 最後一個 stage
- WHEN 某服務的
build:未指定target: - THEN Docker 依規範建置到 Dockerfile 中定義的最後一個 stage,可能與該服務實際需要的 stage 不同——此為本次 multi-stage 化後所有服務都必須明確宣告 target 的原因
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-platformimage 的服務容器、以及nginx(依賴cfdive-nginx-webimage)皆以新 image 重新建立
Scenario: Migration 在新 container 就緒後才執行
- WHEN
docker compose up -d執行完成 - THEN 部署流程等待
appcontainer 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
nginxcontainer 啟動 - 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 內容中