Compare commits
3 Commits
9a1de393f6
...
286db8ffe2
| Author | SHA1 | Date | |
|---|---|---|---|
| 286db8ffe2 | |||
| 6a7d98b0fe | |||
| 3b0cdf96d8 |
@@ -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 "$@"
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-04
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
## 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。目前無待決問題。)
|
||||||
|
|
||||||
|
## 實作驗證後追加(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` 的強制覆蓋清單是否需要同步更新**。
|
||||||
@@ -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`(新增,含 `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 的服務)
|
||||||
|
- **受影響流程**:VPS 部署流程(Gitea Actions self-hosted runner)、本機開發流程(開發者需改用 `docker compose build` 才能讓程式碼變動生效,或另外規劃開發環境仍用 bind mount 的 override 方案)
|
||||||
|
- **不受影響**:`file-storage-s3`(本次不強制切換到 S3,上傳檔案改走具名 volume 過渡)、`compose-cloud-baseline`(healthcheck/開發工具隔離邏輯不變)
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
## 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: 每個 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
|
||||||
|
`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,71 @@
|
|||||||
|
## 1. Dockerfile 改為 multi-stage:app 階段內嵌程式碼與相依套件
|
||||||
|
|
||||||
|
- [x] 1.1 [後端] `Dockerfile`:第一階段命名為 `FROM php:8.2-fpm AS app`(既有內容套用此階段名稱,供後續 `web` 階段 `COPY --from=app` 使用)
|
||||||
|
- [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 會失敗)
|
||||||
|
- [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)
|
||||||
|
- [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` 正確產生)
|
||||||
|
- [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 指令
|
||||||
|
- [x] 1.6 [後端] 新增 `.dockerignore`(目前不存在,屬本次必要項目非優化):排除 `vendor/`、`node_modules/`、`.env`、`storage/app/public/*`,避免機敏資訊(`.env` 內資料庫密碼)或本機殘留檔案被 `COPY . /var/www` 複製進 image
|
||||||
|
|
||||||
|
## 2. Dockerfile 新增 web 階段(nginx 程式碼來源)
|
||||||
|
|
||||||
|
- [x] 2.1 [後端] `Dockerfile`:新增第二階段 `FROM nginx:alpine AS web`
|
||||||
|
- [x] 2.2 [後端] `Dockerfile`:`web` 階段執行 `COPY --from=app /var/www/public /var/www/public`,取得 `app` 階段建置完成的 `public/` 目錄
|
||||||
|
- [x] 2.3 [後端] `Dockerfile`:`web` 階段**再次獨立執行** `RUN ln -sfn ../storage/app/public /var/www/public/storage`(不依賴 2.2 的 `COPY --from` 隱性帶入 1.5 建立的符號連結——兩階段各自明確宣告,避免日後若複製方式調整導致符號連結無聲消失)
|
||||||
|
- [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)
|
||||||
|
|
||||||
|
- [x] 3.1 [後端] `docker-compose.yml`:移除 `app`、`nginx`、`scheduler`、`queue-worker`、`reverb` 五個服務的 `./:/var/www` volume 定義
|
||||||
|
- [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)
|
||||||
|
- [x] 3.3 [後端] `docker-compose.yml`:新增具名 volume `storage-app-public`(定名,見 design.md Decision 3),掛載至 `app` 與 `nginx` 服務容器內的 **`/var/www/storage/app/public`**(絕對路徑),並在底部 `volumes:` 區塊註冊
|
||||||
|
- [x] 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
|
||||||
|
|
||||||
|
- [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 全部移除)
|
||||||
|
|
||||||
|
- [x] 5.1 [後端] `docker/php/docker-entrypoint.sh`:移除「`.env` 不存在則複製 `.env.example` + `key:generate`」區塊
|
||||||
|
- [x] 5.2 [後端] `docker/php/docker-entrypoint.sh`:移除「依 `composer.lock` 內容比對決定是否重新 `composer install`」區塊
|
||||||
|
- [x] 5.3 [後端] `docker/php/docker-entrypoint.sh`:移除強制改寫 `DB_HOST=db` 的 `preg_replace` 邏輯(改由 `.env`/compose `environment:` 直接提供正確值)
|
||||||
|
- [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)
|
||||||
|
- [x] 5.5 [後端] `docker/php/docker-entrypoint.sh`:移除 `storage:link` 執行(symlink 已於 1.5 在 image build 階段建立,執行期不需要)
|
||||||
|
- [x] 5.6 [後端] `docker/php/docker-entrypoint.sh`:確認保留目錄與權限設定(`storage`、`bootstrap/cache`)作為 entrypoint 最終僅存的啟動必要工作
|
||||||
|
|
||||||
|
## 6. 部署流程(.gitea/workflows/deploy.yml)
|
||||||
|
|
||||||
|
- [x] 6.1 [後端] `deploy.yml`:`git reset --hard origin/master` 之後新增 `docker compose build` step
|
||||||
|
- [x] 6.2 [後端] `deploy.yml`:把原本「Install Composer dependencies」step 移除(已在 image build 階段完成)
|
||||||
|
- [x] 6.3 [後端] `deploy.yml`:新增 `docker compose up -d --remove-orphans` step,並加入等待 `app` container healthy 的檢查(如輪詢 `docker compose ps --format json` 或既有 healthcheck 狀態)後才繼續
|
||||||
|
- [x] 6.4 [後端] `deploy.yml`:確認 migration、`config:cache`/`route:cache`/`view:cache`/`event:cache`、`l5-swagger:generate` 幾個 step 移到 `app` healthy 之後執行
|
||||||
|
- [x] 6.5 [後端] `deploy.yml`:移除原本獨立的「Restart queue worker」step(`docker compose up -d` 已會因 image 變更重建該 container)
|
||||||
|
|
||||||
|
## 7. 驗證
|
||||||
|
|
||||||
|
- [x] 7.1 [整合測試] 本機執行 `docker compose build && docker compose up -d`(含 override),確認五個服務容器全部啟動、`app` healthcheck(`nc -z 127.0.0.1 9000`)通過
|
||||||
|
- [x] 7.2 [整合測試] 本機容器內執行 `php artisan test`,確認既有測試套件全綠(比對變更前的 baseline 通過數)——239 passed/578 assertions(dev override 下 vendor 含 dev 依賴);發現並修復 `env_file:` 導致 `.env` 全部變數(非僅 DB_*)洩漏進容器成真實環境變數、覆蓋 `phpunit.xml` 測試設定值的問題,詳見下方「實作期間發現並修復」
|
||||||
|
- [x] 7.3 [整合測試] 驗證本機開發體驗:override 生效時修改一個 PHP 檔案,確認容器內立即反映變更(不需 rebuild)——修改 `tests/bootstrap.php`/`phpunit.xml` 後未 rebuild 即生效,驗證通過
|
||||||
|
- [x] 7.4 [整合測試] 驗證 production 模式:`docker compose -f docker-compose.yml up -d`(不含 override)啟動後確認容器仍正常回應 API 請求——首頁 200、`/api/diving-offers` 回傳正確 JSON、migration/cache/swagger 全部成功
|
||||||
|
- [x] 7.5 [整合測試] 驗證上傳檔案持久性:建立測試檔案於 `storage/app/public/`,`docker compose up -d --force-recreate app nginx` 重建容器,確認該檔案透過 nginx `/storage/...` 仍可正常存取
|
||||||
|
- [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 已知風險項)——需 VPS/CI 環境,留給 Hank 執行
|
||||||
|
- [ ] 7.8 [整合測試] VPS 正式部署後,人工驗證預約、聊天室、課程圖片上傳、教練證照上傳等現有功能正常,並確認 `scheduler`/`queue-worker`/`reverb` 皆以新 image 啟動(`docker compose ps` 確認 image ID 一致)——需 VPS 環境,留給 Hank 執行
|
||||||
|
- [x] 7.9 [整合測試] 確認 entrypoint/deploy 職責切分無重疊:`docker compose up -d` 啟動 `app`/`scheduler`/`queue-worker` 三個共用 entrypoint 的服務時,觀察其 log 不應出現 migration 或 `l5-swagger:generate` 執行紀錄,這些只應出現在 `deploy.yml` 執行紀錄裡——三個容器 log 都只有初始化與 php-fpm 啟動訊息
|
||||||
|
- [x] 7.10 [整合測試] 驗證 nginx 靜態資源獨立於 app:停掉 `app` container 後 nginx 仍能透過 HTTP 直接回應 `public/` 下的靜態檔案(`favicon.ico` 200)與 production 模式下 `/` 回應 200,確認不依賴 `app` container 是否存在或執行中
|
||||||
|
- [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 內
|
||||||
|
- [x] 7.12 [整合測試] 驗證 nginx 透過 public/storage 正確服務已上傳檔案:停掉 `app` container 後直接對 nginx 發出 `/storage/test2/file.txt` 請求,回應內容正確(200),確認全程未經過 `app` container
|
||||||
|
- [x] 7.13 [整合測試] 驗證 composer 自動發現正確:`docker compose exec app php artisan route:list` 正常回傳完整路由清單,`l5-swagger:generate` 正常執行,確認 `package:discover` 於 build 階段正確執行
|
||||||
|
- [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