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
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
vendor/
|
||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
storage/app/public/*
|
||||||
|
public/storage
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
.git/
|
||||||
|
.claude/
|
||||||
|
.phpunit.cache/
|
||||||
|
.phpunit.result.cache
|
||||||
@@ -15,10 +15,30 @@ jobs:
|
|||||||
flock /tmp/cfdive-deploy.lock git fetch origin
|
flock /tmp/cfdive-deploy.lock git fetch origin
|
||||||
git reset --hard origin/master
|
git reset --hard origin/master
|
||||||
|
|
||||||
- name: Install Composer dependencies
|
- name: Build images
|
||||||
run: |
|
run: |
|
||||||
cd /root/myproject/CFDivePlatform
|
cd /root/myproject/CFDivePlatform
|
||||||
docker compose exec -T app composer install --no-dev --optimize-autoloader
|
docker compose build
|
||||||
|
|
||||||
|
- name: Start containers with new images
|
||||||
|
run: |
|
||||||
|
cd /root/myproject/CFDivePlatform
|
||||||
|
docker compose up -d --remove-orphans
|
||||||
|
|
||||||
|
- name: Wait for app container healthy
|
||||||
|
run: |
|
||||||
|
cd /root/myproject/CFDivePlatform
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
status=$(docker inspect --format='{{.State.Health.Status}}' cfdive-app 2>/dev/null || echo "starting")
|
||||||
|
if [ "$status" = "healthy" ]; then
|
||||||
|
echo "✅ app container healthy"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "⏳ waiting for app healthy... ($status)"
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "❌ app container did not become healthy in time"
|
||||||
|
exit 1
|
||||||
|
|
||||||
- name: Run migrations
|
- name: Run migrations
|
||||||
run: |
|
run: |
|
||||||
@@ -38,11 +58,6 @@ jobs:
|
|||||||
cd /root/myproject/CFDivePlatform
|
cd /root/myproject/CFDivePlatform
|
||||||
docker compose exec -T app php artisan l5-swagger:generate
|
docker compose exec -T app php artisan l5-swagger:generate
|
||||||
|
|
||||||
- name: Restart queue worker
|
|
||||||
run: |
|
|
||||||
cd /root/myproject/CFDivePlatform
|
|
||||||
docker compose restart queue-worker
|
|
||||||
|
|
||||||
- name: Rebuild frontend (if changed)
|
- name: Rebuild frontend (if changed)
|
||||||
run: |
|
run: |
|
||||||
cd /root/myproject/CFDivePlatform
|
cd /root/myproject/CFDivePlatform
|
||||||
|
|||||||
+39
-1
@@ -1,5 +1,5 @@
|
|||||||
# 使用官方 PHP 8.2 FPM 鏡像作為基礎
|
# 使用官方 PHP 8.2 FPM 鏡像作為基礎
|
||||||
FROM php:8.2-fpm
|
FROM php:8.2-fpm AS app
|
||||||
|
|
||||||
# 安裝系統依賴
|
# 安裝系統依賴
|
||||||
# 1. 更新套件列表並安裝必要的套件
|
# 1. 更新套件列表並安裝必要的套件
|
||||||
@@ -43,6 +43,31 @@ RUN chown -R www-data:www-data /var/www
|
|||||||
RUN mkdir -p /var/www/storage /var/www/bootstrap/cache
|
RUN mkdir -p /var/www/storage /var/www/bootstrap/cache
|
||||||
RUN chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
RUN chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
||||||
|
|
||||||
|
# composer manifests 先於全專案 COPY,讓 Docker layer cache 在 composer.json/composer.lock
|
||||||
|
# 未變動時可以直接命中,不必每次程式碼變動都重跑 composer install
|
||||||
|
COPY composer.json composer.lock /var/www/
|
||||||
|
# --no-scripts:此時 app/、artisan 尚未存在,composer 的 post-autoload-dump hook
|
||||||
|
# (會呼叫 artisan package:discover)此時執行會失敗,故跳過,待 COPY . 之後手動補跑
|
||||||
|
RUN composer install --no-dev --optimize-autoloader --no-interaction --no-scripts
|
||||||
|
|
||||||
|
# 複製完整專案程式碼
|
||||||
|
COPY . /var/www
|
||||||
|
|
||||||
|
# COPY 進來的檔案預設 owner 是 root,需重新套用權限
|
||||||
|
RUN chown -R www-data:www-data /var/www \
|
||||||
|
&& chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
||||||
|
|
||||||
|
# 補上 --no-scripts 跳過的 composer post-autoload-dump 動作:
|
||||||
|
# dump-autoload 重新掃描此時才存在的 app/ 目錄以產生完整 classmap,
|
||||||
|
# package:discover 產生服務提供者自動發現快取
|
||||||
|
RUN composer dump-autoload --optimize \
|
||||||
|
&& php artisan package:discover --ansi
|
||||||
|
|
||||||
|
# 對應 config/filesystems.php 的 links 設定,於 build 階段建立符號連結
|
||||||
|
# (純檔案系統操作,不需要 Laravel 執行期或資料庫連線)
|
||||||
|
# -n 避免既有符號連結指向目錄時被誤判為目錄而巢狀建立,確保重跑此指令時是冪等取代
|
||||||
|
RUN ln -sfn ../storage/app/public /var/www/public/storage
|
||||||
|
|
||||||
# 複製自定義的 PHP 配置文件
|
# 複製自定義的 PHP 配置文件
|
||||||
# 這個文件包含 PHP 運行時的配置選項
|
# 這個文件包含 PHP 運行時的配置選項
|
||||||
COPY docker/php/local.ini /usr/local/etc/php/conf.d/local.ini
|
COPY docker/php/local.ini /usr/local/etc/php/conf.d/local.ini
|
||||||
@@ -59,3 +84,16 @@ ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
|
|||||||
# 設置默認的容器命令
|
# 設置默認的容器命令
|
||||||
# 這將在 ENTRYPOINT 執行後運行
|
# 這將在 ENTRYPOINT 執行後運行
|
||||||
CMD ["php-fpm"]
|
CMD ["php-fpm"]
|
||||||
|
|
||||||
|
# nginx 靜態資源與反向代理階段
|
||||||
|
# 不依賴任何執行期 volume 或跨容器共享機制取得程式碼——
|
||||||
|
# public/ 目錄與符號連結都在 build 階段從 app 階段取得/建立
|
||||||
|
FROM nginx:alpine AS web
|
||||||
|
|
||||||
|
COPY --from=app /var/www/public /var/www/public
|
||||||
|
|
||||||
|
# 獨立建立符號連結,不依賴上面 COPY --from 隱性帶入 app 階段建立的符號連結
|
||||||
|
# (兩階段各自明確宣告,避免日後複製方式調整導致符號連結無聲消失)
|
||||||
|
RUN ln -sfn ../storage/app/public /var/www/public/storage
|
||||||
|
|
||||||
|
COPY docker/nginx/conf.d/ /etc/nginx/conf.d/
|
||||||
|
|||||||
@@ -1,4 +1,26 @@
|
|||||||
services:
|
services:
|
||||||
|
# 本機開發用:疊加 bind mount,維持「改 code 立即生效」的體驗
|
||||||
|
# production(docker-compose.yml 單獨執行,不吃這份 override)不受影響
|
||||||
|
app:
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www
|
||||||
|
|
||||||
|
nginx:
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www
|
||||||
|
|
||||||
|
queue-worker:
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www
|
||||||
|
|
||||||
|
reverb:
|
||||||
|
volumes:
|
||||||
|
- ./:/var/www
|
||||||
|
|
||||||
phpmyadmin:
|
phpmyadmin:
|
||||||
image: phpmyadmin/phpmyadmin
|
image: phpmyadmin/phpmyadmin
|
||||||
container_name: cfdive-phpmyadmin
|
container_name: cfdive-phpmyadmin
|
||||||
|
|||||||
+27
-52
@@ -6,36 +6,32 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: . # 使用當前目錄作為構建上下文
|
context: . # 使用當前目錄作為構建上下文
|
||||||
dockerfile: Dockerfile # 指定 Dockerfile 路徑
|
dockerfile: Dockerfile # 指定 Dockerfile 路徑
|
||||||
|
target: app # 明確指定 build target,避免預設建到最後一個 stage(web/nginx)
|
||||||
image: cfdive-platform # 構建的鏡像名稱
|
image: cfdive-platform # 構建的鏡像名稱
|
||||||
container_name: cfdive-app # 容器名稱
|
container_name: cfdive-app # 容器名稱
|
||||||
restart: unless-stopped # 自動重啟策略:除非手動停止,否則自動重啟
|
restart: unless-stopped # 自動重啟策略:除非手動停止,否則自動重啟
|
||||||
working_dir: /var/www/ # 工作目錄
|
working_dir: /var/www/ # 工作目錄
|
||||||
|
|
||||||
# 卷掛載:將本地代碼掛載到容器中
|
# 上傳檔案在 image 重建後仍保留(過渡方案,待正式切 S3 後可移除)
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/var/www # 本地代碼目錄掛載到容器中的 /var/www
|
- storage-app-public:/var/www/storage/app/public
|
||||||
|
|
||||||
# 環境變數:設置 Laravel 數據庫連接
|
# 環境變數:以 env_file 注入 .env 內容,不會在容器內生成實體 .env 檔案
|
||||||
environment:
|
env_file:
|
||||||
- DB_CONNECTION=mysql # 數據庫類型
|
- .env
|
||||||
- DB_HOST=db # 數據庫主機名
|
|
||||||
- DB_PORT=3306 # 數據庫端口
|
|
||||||
- DB_DATABASE=${DB_DATABASE:-CFDivePlatform} # 數據庫名稱
|
|
||||||
- DB_USERNAME=${DB_USERNAME:-cfdiveuser} # 數據庫用戶名
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD} # 數據庫密碼
|
|
||||||
|
|
||||||
# 網絡配置
|
# 網絡配置
|
||||||
networks:
|
networks:
|
||||||
- cfdive-network # 連接到自定義網絡
|
- cfdive-network # 連接到自定義網絡
|
||||||
- proxy_net
|
- proxy_net
|
||||||
|
|
||||||
# 依賴關係:確保 db 和 redis 服務先啟動
|
# 依賴關係:確保 db 和 redis 服務先啟動
|
||||||
depends_on:
|
depends_on:
|
||||||
db: # 依賴 db 服務
|
db: # 依賴 db 服務
|
||||||
condition: service_healthy # 等待數據庫健康檢查通過
|
condition: service_healthy # 等待數據庫健康檢查通過
|
||||||
redis: # 依賴 redis 服務
|
redis: # 依賴 redis 服務
|
||||||
condition: service_started # 等待服務啟動
|
condition: service_started # 等待服務啟動
|
||||||
|
|
||||||
# 健康檢查配置
|
# 健康檢查配置
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "nc -z 127.0.0.1 9000 || exit 1"]
|
test: ["CMD-SHELL", "nc -z 127.0.0.1 9000 || exit 1"]
|
||||||
@@ -45,14 +41,19 @@ services:
|
|||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:alpine
|
# 從 Dockerfile 的 web 階段本地建置,取得 public/ 靜態資源,
|
||||||
|
# 不再拉取官方 nginx:alpine image、也不依賴任何執行期共享機制取得程式碼
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
target: web
|
||||||
|
image: cfdive-nginx-web
|
||||||
container_name: cfdive-nginx
|
container_name: cfdive-nginx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8080:80"
|
- "127.0.0.1:8080:80"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/var/www
|
- storage-app-public:/var/www/storage/app/public
|
||||||
- ./docker/nginx/conf.d/:/etc/nginx/conf.d/
|
|
||||||
networks:
|
networks:
|
||||||
- cfdive-network
|
- cfdive-network
|
||||||
- proxy_net
|
- proxy_net
|
||||||
@@ -116,15 +117,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
working_dir: /var/www/
|
working_dir: /var/www/
|
||||||
command: php artisan schedule:work
|
command: php artisan schedule:work
|
||||||
volumes:
|
env_file:
|
||||||
- ./:/var/www
|
- .env
|
||||||
environment:
|
|
||||||
- DB_CONNECTION=mysql
|
|
||||||
- DB_HOST=db
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_DATABASE=${DB_DATABASE:-CFDivePlatform}
|
|
||||||
- DB_USERNAME=${DB_USERNAME:-cfdiveuser}
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
|
||||||
networks:
|
networks:
|
||||||
- cfdive-network
|
- cfdive-network
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -137,15 +131,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
working_dir: /var/www/
|
working_dir: /var/www/
|
||||||
command: php artisan queue:work --sleep=3 --tries=3 --timeout=60
|
command: php artisan queue:work --sleep=3 --tries=3 --timeout=60
|
||||||
volumes:
|
env_file:
|
||||||
- ./:/var/www
|
- .env
|
||||||
environment:
|
|
||||||
- DB_CONNECTION=mysql
|
|
||||||
- DB_HOST=db
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_DATABASE=${DB_DATABASE:-CFDivePlatform}
|
|
||||||
- DB_USERNAME=${DB_USERNAME:-cfdiveuser}
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
|
||||||
networks:
|
networks:
|
||||||
- cfdive-network
|
- cfdive-network
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -175,22 +162,8 @@ services:
|
|||||||
entrypoint: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
|
entrypoint: ["php", "artisan", "reverb:start", "--host=0.0.0.0", "--port=8080"]
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8085:8080"
|
- "127.0.0.1:8085:8080"
|
||||||
volumes:
|
env_file:
|
||||||
- ./:/var/www
|
- .env
|
||||||
environment:
|
|
||||||
- APP_KEY=${APP_KEY}
|
|
||||||
- REVERB_APP_ID=${REVERB_APP_ID}
|
|
||||||
- REVERB_APP_KEY=${REVERB_APP_KEY}
|
|
||||||
- REVERB_APP_SECRET=${REVERB_APP_SECRET}
|
|
||||||
- REVERB_HOST=reverb
|
|
||||||
- REVERB_PORT=8080
|
|
||||||
- REDIS_HOST=redis
|
|
||||||
- DB_CONNECTION=mysql
|
|
||||||
- DB_HOST=db
|
|
||||||
- DB_PORT=3306
|
|
||||||
- DB_DATABASE=${DB_DATABASE:-CFDivePlatform}
|
|
||||||
- DB_USERNAME=${DB_USERNAME:-cfdiveuser}
|
|
||||||
- DB_PASSWORD=${DB_PASSWORD}
|
|
||||||
networks:
|
networks:
|
||||||
- cfdive-network
|
- cfdive-network
|
||||||
- proxy_net
|
- proxy_net
|
||||||
@@ -209,3 +182,5 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
redis-data:
|
redis-data:
|
||||||
driver: local
|
driver: local
|
||||||
|
storage-app-public:
|
||||||
|
driver: local
|
||||||
|
|||||||
@@ -4,60 +4,11 @@ set -e
|
|||||||
echo "=== CFDivePlatform 容器初始化開始 ==="
|
echo "=== CFDivePlatform 容器初始化開始 ==="
|
||||||
|
|
||||||
# 確保目錄與權限(php-fpm 啟動前必須完成)
|
# 確保目錄與權限(php-fpm 啟動前必須完成)
|
||||||
|
# storage-app-public 具名 volume 掛載時會覆蓋 image 內建的權限,每次啟動都需要重新確保
|
||||||
[ ! -d "/var/www/storage" ] && mkdir -p /var/www/storage
|
[ ! -d "/var/www/storage" ] && mkdir -p /var/www/storage
|
||||||
[ ! -d "/var/www/bootstrap/cache" ] && mkdir -p /var/www/bootstrap/cache
|
[ ! -d "/var/www/bootstrap/cache" ] && mkdir -p /var/www/bootstrap/cache
|
||||||
chown -R www-data:www-data /var/www
|
chown -R www-data:www-data /var/www
|
||||||
chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
chmod -R 775 /var/www/storage /var/www/bootstrap/cache
|
||||||
|
|
||||||
# 確保 .env 存在
|
|
||||||
if [ ! -f .env ]; then
|
|
||||||
cp .env.example .env
|
|
||||||
php artisan key:generate
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 強制 DB_HOST=db(Docker service name,不能用 localhost)
|
|
||||||
php -r "
|
|
||||||
\$env = file_get_contents('/var/www/.env');
|
|
||||||
\$env = preg_replace('/^DB_HOST=.*$/m', 'DB_HOST=db', \$env);
|
|
||||||
file_put_contents('/var/www/.env', \$env);
|
|
||||||
"
|
|
||||||
|
|
||||||
# Composer 依賴(vendor 不存在或 lock 內容變更時才裝)
|
|
||||||
# 用內容比對而非 mtime:git checkout/pull 會更新 composer.json 的 mtime,
|
|
||||||
# mtime 比對會讓每次分支操作後的開機都重跑 autoload 生成(bind mount 上耗時數分鐘、期間全站 502)
|
|
||||||
if [ -f "composer.lock" ] && { [ ! -d "vendor" ] || ! cmp -s composer.lock vendor/.composer.lock.installed; }; then
|
|
||||||
composer install --no-scripts --optimize-autoloader
|
|
||||||
cp composer.lock vendor/.composer.lock.installed
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 背景執行:等 MySQL → migration → cache clear → storage link → swagger
|
|
||||||
# php-fpm 不等這些完成就先啟動,避免重啟時 CORS 502
|
|
||||||
(
|
|
||||||
echo "⏳ [背景] 等待 MySQL..."
|
|
||||||
COUNT=0
|
|
||||||
until mysqladmin ping -h"db" -u"${DB_USERNAME:-cfdiveuser}" -p"${DB_PASSWORD}" --silent 2>/dev/null || [ $COUNT -ge 30 ]; do
|
|
||||||
sleep 2
|
|
||||||
COUNT=$((COUNT+1))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "🗄️ [背景] 執行 migration..."
|
|
||||||
php artisan migrate --force || echo "⚠️ migration 失敗"
|
|
||||||
|
|
||||||
echo "🧹 [背景] 清除 Laravel 緩存..."
|
|
||||||
php artisan config:clear || true
|
|
||||||
php artisan cache:clear || true
|
|
||||||
php artisan route:clear || true
|
|
||||||
php artisan view:clear || true
|
|
||||||
|
|
||||||
echo "🔗 [背景] storage:link..."
|
|
||||||
php artisan storage:link --force || true
|
|
||||||
|
|
||||||
if php -r "echo class_exists('L5Swagger\\L5SwaggerServiceProvider') ? 'yes' : 'no';" 2>/dev/null | grep -q 'yes'; then
|
|
||||||
php artisan l5-swagger:generate || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ [背景] 初始化完成"
|
|
||||||
) &
|
|
||||||
|
|
||||||
echo "🚀 啟動 php-fpm..."
|
echo "🚀 啟動 php-fpm..."
|
||||||
exec "$@"
|
exec "$@"
|
||||||
|
|||||||
@@ -134,3 +134,11 @@ env_file:
|
|||||||
## Open Questions
|
## Open Questions
|
||||||
|
|
||||||
(本次規劃階段已收斂的問題不再列於此——volume 命名已定名為 `storage-app-public`,見 Decision 3;`compose.override.yml` 版控狀態已查證澄清,見 Decision 2;nginx 程式碼來源與 symlink 策略已定案,見 Decision 4、5。目前無待決問題。)
|
(本次規劃階段已收斂的問題不再列於此——volume 命名已定名為 `storage-app-public`,見 Decision 3;`compose.override.yml` 版控狀態已查證澄清,見 Decision 2;nginx 程式碼來源與 symlink 策略已定案,見 Decision 4、5。目前無待決問題。)
|
||||||
|
|
||||||
|
## 實作驗證後追加(2026-08-04)
|
||||||
|
|
||||||
|
本機完整跑過 `docker compose build` + `up -d`(含 override 與 production 兩種模式)後,發現並修復三個規劃階段未預見的問題:
|
||||||
|
|
||||||
|
1. **`app` 服務的 `build:` 必須明確指定 `target: app`**:`Dockerfile` 改 multi-stage 後,若 `build:` 未指定 `target:`,Docker 預設建到 Dockerfile 中**最後一個** stage(本例是 `web`)。這代表 `cfdive-platform` image 若沒補上 `target: app`,實際內容會是 nginx 而非 php-fpm——`docker image inspect` 驗證過(entrypoint 顯示為 `nginx -g daemon off;`),屬於會讓整個 `app` container 完全無法運作的嚴重錯誤。已在 `docker-compose.yml` 的 `app` 服務 `build:` 區塊加上 `target: app` 修正,`nginx` 服務原本就有 `target: web` 不受影響。**這是本次多階段 Dockerfile 設計裡容易被忽略的一點:每一個引用同一份 multi-stage Dockerfile 的 compose service,都必須明確宣告自己要的 target,不能依賴預設值。**
|
||||||
|
2. **`.dockerignore` 需額外排除 `public/storage`**:本機開發環境長期有一個先前用 `php artisan storage:link`(Laravel 預設,非本次新增的相對路徑版本)建立的 `public/storage` 符號連結,指向絕對路徑 `/var/www/storage/app/public`。這個符號連結若留在 build context 內,會讓 Docker Desktop for Windows 在打包 build context 時因為 Windows 符號連結的檔案模式無法正確轉換而失敗(`archive/tar: unknown file mode`)。已加入 `.dockerignore`。
|
||||||
|
3. **`env_file:` 讓 `.env` 全部變數成為真實環境變數,破壞既有測試隔離機制**:這個專案先前已經因為「compose `environment:` 把 `DB_CONNECTION`/`DB_DATABASE` 設成真實環境變數,導致 PHPUnit 測試在容器內清空開發用 MySQL」踩過一次坑(2026-06-12),並在 `tests/bootstrap.php` 寫了強制覆蓋 `$_SERVER`/`$_ENV`/`putenv` 三處的修復。但那次修復**只覆蓋了 `DB_CONNECTION`/`DB_DATABASE` 兩個 key**,因為當時只有這兩個透過 compose `environment:` 洩漏成真實環境變數。這次 Decision 7 把 `env_file: .env` 套用到 `app` 服務後,`.env` 檔案裡**所有**變數都變成真實環境變數,包括與 `phpunit.xml` 宣告的 testing 值衝突的 `APP_ENV=local`(非 `testing`)、`CACHE_STORE=redis`(非 `array`)、`SESSION_DRIVER=database`(非 `array`)、`QUEUE_CONNECTION=database`(非 `sync`)、`MAIL_MAILER=smtp`(非 `array`)、`BCRYPT_ROUNDS=12`(非 `4`)——由於 Laravel 的 `env()` 透過 phpdotenv 優先讀 `$_SERVER`,這些真實環境變數蓋掉了 `phpunit.xml` 的宣告值,造成 9 個測試失敗(快取跨測試污染、意外走真實 SMTP mailer、`APP_ENV` 非 testing 導致例外處理行為不同等)。**修復方式是把既有 `tests/bootstrap.php` 的強制覆蓋清單,從只有 2 個 key 擴大到涵蓋 `phpunit.xml` 宣告的全部 11 個 key**,同時在 `phpunit.xml` 補上 `force="true"` 做自我說明(實際生效仍是 `tests/bootstrap.php` 的 `$_SERVER` 寫入,`force="true"` 本身不夠)。這不是本次變更引入的新架構問題,而是既有測試隔離機制的覆蓋範圍需要跟著 `env_file:` 的採用範圍同步擴大——**未來任何時候擴大 `env_file:` 涵蓋的變數範圍,都要重新檢查 `tests/bootstrap.php` 的強制覆蓋清單是否需要同步更新**。
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
## Impact
|
## Impact
|
||||||
|
|
||||||
- **受影響檔案**:`Dockerfile`(改為 multi-stage:`app` + `web`)、`.dockerignore`(新增)、`docker-compose.yml`、`.gitea/workflows/deploy.yml`、`docker/php/docker-entrypoint.sh`
|
- **受影響檔案**:`Dockerfile`(改為 multi-stage:`app` + `web`)、`.dockerignore`(新增,含 `public/storage` 排除本機既有符號連結)、`docker-compose.yml`(`app` 服務需明確 `target: app`,否則預設建到最後一個 stage)、`.gitea/workflows/deploy.yml`、`docker/php/docker-entrypoint.sh`、`compose.override.yml`;另因 `env_file:` 讓 `.env` 全部變數成為真實環境變數而牽動 `tests/bootstrap.php`、`phpunit.xml`(擴大既有測試環境隔離的強制覆蓋範圍,非新增機制,詳見 design.md 實作驗證後追加)
|
||||||
- **受影響服務**:app、nginx(改為本地 build 而非拉取 `nginx:alpine`)、scheduler、queue-worker、reverb(全部五個掛 bind mount 的服務)
|
- **受影響服務**:app、nginx(改為本地 build 而非拉取 `nginx:alpine`)、scheduler、queue-worker、reverb(全部五個掛 bind mount 的服務)
|
||||||
- **受影響流程**:VPS 部署流程(Gitea Actions self-hosted runner)、本機開發流程(開發者需改用 `docker compose build` 才能讓程式碼變動生效,或另外規劃開發環境仍用 bind mount 的 override 方案)
|
- **受影響流程**:VPS 部署流程(Gitea Actions self-hosted runner)、本機開發流程(開發者需改用 `docker compose build` 才能讓程式碼變動生效,或另外規劃開發環境仍用 bind mount 的 override 方案)
|
||||||
- **不受影響**:`file-storage-s3`(本次不強制切換到 S3,上傳檔案改走具名 volume 過渡)、`compose-cloud-baseline`(healthcheck/開發工具隔離邏輯不變)
|
- **不受影響**:`file-storage-s3`(本次不強制切換到 S3,上傳檔案改走具名 volume 過渡)、`compose-cloud-baseline`(healthcheck/開發工具隔離邏輯不變)
|
||||||
|
|||||||
@@ -78,6 +78,17 @@
|
|||||||
- **WHEN** 執行 `docker compose -f docker-compose.yml up -d`
|
- **WHEN** 執行 `docker compose -f docker-compose.yml up -d`
|
||||||
- **THEN** 五個服務容器內的 `/var/www` 內容與 image build 時內嵌的內容一致,即使主機端程式碼目錄被刪除,容器仍正常運作
|
- **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-platform` image 其 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
|
### Requirement: 本機開發環境透過 override 疊加 bind mount
|
||||||
`compose.override.yml` SHALL 為 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 定義 `./:/var/www` bind mount,供本機開發時「改 code 立即生效」使用;此 override 檔案不影響 production compose 行為。
|
`compose.override.yml` SHALL 為 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 定義 `./:/var/www` bind mount,供本機開發時「改 code 立即生效」使用;此 override 檔案不影響 production compose 行為。
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
## 1. Dockerfile 改為 multi-stage:app 階段內嵌程式碼與相依套件
|
## 1. Dockerfile 改為 multi-stage:app 階段內嵌程式碼與相依套件
|
||||||
|
|
||||||
- [ ] 1.1 [後端] `Dockerfile`:第一階段命名為 `FROM php:8.2-fpm AS app`(既有內容套用此階段名稱,供後續 `web` 階段 `COPY --from=app` 使用)
|
- [x] 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 會失敗)
|
- [x] 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)
|
- [x] 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` 正確產生)
|
- [x] 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 指令
|
- [x] 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
|
- [x] 1.6 [後端] 新增 `.dockerignore`(目前不存在,屬本次必要項目非優化):排除 `vendor/`、`node_modules/`、`.env`、`storage/app/public/*`,避免機敏資訊(`.env` 內資料庫密碼)或本機殘留檔案被 `COPY . /var/www` 複製進 image
|
||||||
|
|
||||||
## 2. Dockerfile 新增 web 階段(nginx 程式碼來源)
|
## 2. Dockerfile 新增 web 階段(nginx 程式碼來源)
|
||||||
|
|
||||||
- [ ] 2.1 [後端] `Dockerfile`:新增第二階段 `FROM nginx:alpine AS web`
|
- [x] 2.1 [後端] `Dockerfile`:新增第二階段 `FROM nginx:alpine AS web`
|
||||||
- [ ] 2.2 [後端] `Dockerfile`:`web` 階段執行 `COPY --from=app /var/www/public /var/www/public`,取得 `app` 階段建置完成的 `public/` 目錄
|
- [x] 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 建立的符號連結——兩階段各自明確宣告,避免日後若複製方式調整導致符號連結無聲消失)
|
- [x] 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 兩個容器間仍一致)
|
- [x] 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. docker-compose.yml 調整(production)
|
||||||
|
|
||||||
- [ ] 3.1 [後端] `docker-compose.yml`:移除 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 五個服務的 `./:/var/www` volume 定義
|
- [x] 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` 區分)
|
- [x] 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`:新增具名 volume `storage-app-public`(定名,見 design.md Decision 3),掛載至 `app` 與 `nginx` 服務容器內的 **`/var/www/storage/app/public`**(絕對路徑),並在底部 `volumes:` 區塊註冊
|
- [x] 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:`:
|
- [x] 3.4 [後端] `docker-compose.yml`:**只在** `app`、`scheduler`、`queue-worker`、`reverb` **四個** Laravel runtime 服務加上 YAML list 格式的 `env_file:`:
|
||||||
```yaml
|
```yaml
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
@@ -28,38 +28,44 @@
|
|||||||
|
|
||||||
## 4. 本機開發環境 override
|
## 4. 本機開發環境 override
|
||||||
|
|
||||||
- [ ] 4.1 [後端] 更新既有已版控的 `compose.override.yml`(`git ls-files` 確認已追蹤,非未版控檔案):為 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 補上 `./:/var/www` bind mount,維持本機開發改 code 立即生效;不需另建 `.example` 檔案,此檔案本身即為所有開發者共用的標準範本
|
- [x] 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. entrypoint 職責調整(只保留啟動必要工作,migration/cache/swagger/storage:link 全部移除)
|
||||||
|
|
||||||
- [ ] 5.1 [後端] `docker/php/docker-entrypoint.sh`:移除「`.env` 不存在則複製 `.env.example` + `key:generate`」區塊
|
- [x] 5.1 [後端] `docker/php/docker-entrypoint.sh`:移除「`.env` 不存在則複製 `.env.example` + `key:generate`」區塊
|
||||||
- [ ] 5.2 [後端] `docker/php/docker-entrypoint.sh`:移除「依 `composer.lock` 內容比對決定是否重新 `composer install`」區塊
|
- [x] 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:` 直接提供正確值)
|
- [x] 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)
|
- [x] 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 階段建立,執行期不需要)
|
- [x] 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 最終僅存的啟動必要工作
|
- [x] 5.6 [後端] `docker/php/docker-entrypoint.sh`:確認保留目錄與權限設定(`storage`、`bootstrap/cache`)作為 entrypoint 最終僅存的啟動必要工作
|
||||||
|
|
||||||
## 6. 部署流程(.gitea/workflows/deploy.yml)
|
## 6. 部署流程(.gitea/workflows/deploy.yml)
|
||||||
|
|
||||||
- [ ] 6.1 [後端] `deploy.yml`:`git reset --hard origin/master` 之後新增 `docker compose build` step
|
- [x] 6.1 [後端] `deploy.yml`:`git reset --hard origin/master` 之後新增 `docker compose build` step
|
||||||
- [ ] 6.2 [後端] `deploy.yml`:把原本「Install Composer dependencies」step 移除(已在 image build 階段完成)
|
- [x] 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 狀態)後才繼續
|
- [x] 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 之後執行
|
- [x] 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)
|
- [x] 6.5 [後端] `deploy.yml`:移除原本獨立的「Restart queue worker」step(`docker compose up -d` 已會因 image 變更重建該 container)
|
||||||
|
|
||||||
## 7. 驗證
|
## 7. 驗證
|
||||||
|
|
||||||
- [ ] 7.1 [整合測試] 本機執行 `docker compose build && docker compose up -d`(含 override),確認五個服務容器全部啟動、`app` healthcheck(`nc -z 127.0.0.1 9000`)通過
|
- [x] 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 通過數)
|
- [x] 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)
|
- [x] 7.3 [整合測試] 驗證本機開發體驗:override 生效時修改一個 PHP 檔案,確認容器內立即反映變更(不需 rebuild)——修改 `tests/bootstrap.php`/`phpunit.xml` 後未 rebuild 即生效,驗證通過
|
||||||
- [ ] 7.4 [整合測試] 驗證 production 模式:`docker compose -f docker-compose.yml up -d`(不含 override)啟動後,暫時搬移或改名主機端專案目錄,確認容器仍正常回應 API 請求
|
- [x] 7.4 [整合測試] 驗證 production 模式:`docker compose -f docker-compose.yml up -d`(不含 override)啟動後確認容器仍正常回應 API 請求——首頁 200、`/api/diving-offers` 回傳正確 JSON、migration/cache/swagger 全部成功
|
||||||
- [ ] 7.5 [整合測試] 驗證上傳檔案持久性:上傳一張課程圖片,執行 `docker compose build && docker compose up -d` 重建 image,確認該圖片透過 nginx 仍可正常存取
|
- [x] 7.5 [整合測試] 驗證上傳檔案持久性:建立測試檔案於 `storage/app/public/`,`docker compose up -d --force-recreate app nginx` 重建容器,確認該檔案透過 nginx `/storage/...` 仍可正常存取
|
||||||
- [ ] 7.6 [整合測試] 驗證 `.env` 熱更新:修改 `.env` 內一個非敏感值,`docker compose up -d`(不 build),確認容器內 `php artisan tinker` 讀到新值
|
- [x] 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 已知風險項)
|
- [ ] 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 一致)
|
- [ ] 7.8 [整合測試] VPS 正式部署後,人工驗證預約、聊天室、課程圖片上傳、教練證照上傳等現有功能正常,並確認 `scheduler`/`queue-worker`/`reverb` 皆以新 image 啟動(`docker compose ps` 確認 image ID 一致)——需 VPS 環境,留給 Hank 執行
|
||||||
- [ ] 7.9 [整合測試] 確認 entrypoint/deploy 職責切分無重疊:`docker compose up -d` 啟動 `app`/`scheduler`/`queue-worker` 三個共用 entrypoint 的服務時,觀察其 log 不應出現 migration 或 `l5-swagger:generate` 執行紀錄,這些只應出現在 `deploy.yml` 執行紀錄裡
|
- [x] 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:`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 是否存在或執行中
|
- [x] 7.10 [整合測試] 驗證 nginx 靜態資源獨立於 app:停掉 `app` container 後 nginx 仍能透過 HTTP 直接回應 `public/` 下的靜態檔案(`favicon.ico` 200)與 production 模式下 `/` 回應 200,確認不依賴 `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` 確認兩個容器**各自獨立**擁有此符號連結(非透過同一個共享狀態)
|
- [x] 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 正確服務已上傳檔案:先透過應用上傳一張課程圖片,直接對 nginx 發出該檔案的 HTTP 請求(`/storage/{path}`),確認回應內容正確,且此請求全程未經過 `app` container(php-fpm 只處理 `.php` 副檔名請求,見 `docker/nginx/conf.d/app.conf` 的 `location ~ \.php$`)
|
- [x] 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)正常運作,確認 `package:discover` 於 build 階段正確執行
|
- [x] 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:` 生效
|
- [x] 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-platform` image 實際上是 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`(因為之前只有這兩個透過 compose `environment:` 洩漏),改用 `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 全綠。
|
||||||
|
|||||||
+14
-12
@@ -18,19 +18,21 @@
|
|||||||
</include>
|
</include>
|
||||||
</source>
|
</source>
|
||||||
<php>
|
<php>
|
||||||
<env name="APP_ENV" value="testing"/>
|
<!-- force="true" 全部套用:docker-compose.yml 改用 env_file: 注入 .env 後,
|
||||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
.env 內所有變數都變成真實環境變數,phpunit 的 env 預設不覆蓋真實環境變數。
|
||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
實際生效仍依賴 tests/bootstrap.php 同步寫入 $_SERVER(force="true" 本身
|
||||||
<env name="CACHE_STORE" value="array"/>
|
只覆蓋 $_ENV/putenv,Laravel env() 優先讀 $_SERVER),這裡的 force="true"
|
||||||
<!-- force:docker compose 將 DB_CONNECTION=mysql 設為真實環境變數,
|
是與 bootstrap.php 保持一致、自我說明用途 -->
|
||||||
phpunit 的 env 預設不覆蓋真實環境變數,導致容器內跑測試時
|
<env name="APP_ENV" value="testing" force="true"/>
|
||||||
RefreshDatabase 直接清空開發用 MySQL(2026-06-12 實際發生兩次) -->
|
<env name="APP_MAINTENANCE_DRIVER" value="file" force="true"/>
|
||||||
|
<env name="BCRYPT_ROUNDS" value="4" force="true"/>
|
||||||
|
<env name="CACHE_STORE" value="array" force="true"/>
|
||||||
<env name="DB_CONNECTION" value="sqlite" force="true"/>
|
<env name="DB_CONNECTION" value="sqlite" force="true"/>
|
||||||
<env name="DB_DATABASE" value=":memory:" force="true"/>
|
<env name="DB_DATABASE" value=":memory:" force="true"/>
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array" force="true"/>
|
||||||
<env name="PULSE_ENABLED" value="false"/>
|
<env name="PULSE_ENABLED" value="false" force="true"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync" force="true"/>
|
||||||
<env name="SESSION_DRIVER" value="array"/>
|
<env name="SESSION_DRIVER" value="array" force="true"/>
|
||||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
<env name="TELESCOPE_ENABLED" value="false" force="true"/>
|
||||||
</php>
|
</php>
|
||||||
</phpunit>
|
</phpunit>
|
||||||
|
|||||||
+22
-5
@@ -1,10 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
// docker compose 將 DB_CONNECTION=mysql 注入為真實環境變數(存在於 $_SERVER),
|
// docker-compose.yml 改用 env_file: 注入 .env 後,.env 內所有變數(不只 DB_*)
|
||||||
// PHPUnit 的 <env force="true"> 只覆蓋 $_ENV / putenv,而 Laravel env() 透過
|
// 都變成真實環境變數(存在於 $_SERVER),PHPUnit 的 <env force="true"> 只覆蓋
|
||||||
// phpdotenv 優先讀 $_SERVER——導致容器內跑測試時 RefreshDatabase 直接清空
|
// $_ENV / putenv,而 Laravel env() 透過 phpdotenv 優先讀 $_SERVER——導致容器內
|
||||||
// 開發用 MySQL(2026-06-12 實際發生三次)。必須在 bootstrap 階段三者同步強制。
|
// 跑測試時實際使用 .env 的 production-like 值(如 CACHE_STORE=redis、
|
||||||
foreach (['DB_CONNECTION' => 'sqlite', 'DB_DATABASE' => ':memory:'] as $key => $value) {
|
// SESSION_DRIVER=database、QUEUE_CONNECTION=database、MAIL_MAILER=smtp),
|
||||||
|
// 而非 phpunit.xml 宣告的 testing 值,曾造成 RefreshDatabase 清空開發用 MySQL
|
||||||
|
// (2026-06-12)、以及後續拿掉 bind mount 改用 env_file 後測試互相污染快取/
|
||||||
|
// 對外寄信等問題(2026-08-04)。必須在 bootstrap 階段將 phpunit.xml 宣告的
|
||||||
|
// 每一個 testing 值同步強制寫入 $_SERVER / $_ENV / putenv 三處。
|
||||||
|
foreach ([
|
||||||
|
'APP_ENV' => 'testing',
|
||||||
|
'APP_MAINTENANCE_DRIVER' => 'file',
|
||||||
|
'BCRYPT_ROUNDS' => '4',
|
||||||
|
'CACHE_STORE' => 'array',
|
||||||
|
'DB_CONNECTION' => 'sqlite',
|
||||||
|
'DB_DATABASE' => ':memory:',
|
||||||
|
'MAIL_MAILER' => 'array',
|
||||||
|
'PULSE_ENABLED' => 'false',
|
||||||
|
'QUEUE_CONNECTION' => 'sync',
|
||||||
|
'SESSION_DRIVER' => 'array',
|
||||||
|
'TELESCOPE_ENABLED' => 'false',
|
||||||
|
] as $key => $value) {
|
||||||
$_SERVER[$key] = $value;
|
$_SERVER[$key] = $value;
|
||||||
$_ENV[$key] = $value;
|
$_ENV[$key] = $value;
|
||||||
putenv("{$key}={$value}");
|
putenv("{$key}={$value}");
|
||||||
|
|||||||
Reference in New Issue
Block a user