🎮 Gameboy-Gate — 每個專案的入學考試
強制要求: 任何部署在 Gameboy(
<GAMEBOY_IP>)上的專案都必須通過此關卡 — 無論誰使用 Claude,無論哪個專案。沒有 PASS 就沒有發佈。
🔒 連線資料在本機:
<GAMEBOY_IP>、<GAMEBOY_SSH>、<COOLIFY_URL>、<WATCHDOG_*>的真實值儲存在 gitignored 的gameboy-gate/gameboy.local.md檔案中(不在公開 repo 中)。每次執行關卡前,請先讀取此檔案並替換掉這些佔位符。
背景:Gameboy 伺服器
伺服器:Hetzner CAX11 (ARM)
IP: <GAMEBOY_IP> # 真實值:請見 gameboy.local.md
SSH: <GAMEBOY_SSH> # 真實值:請見 gameboy.local.md
RAM: 4GB 總計
CPU: 2x vCPU (ARM Neoverse-N1 @ 2.0GHz)
磁碟: 40GB SSD
OS: Ubuntu (Docker + Coolify)
Coolify UI:<COOLIFY_URL> # 真實值:請見 gameboy.local.md
固定開銷保留(絕不低於此值):
OS + 核心: 400MB
Coolify(含 DB、Redis、Proxy、Sentinel):900MB
安全緩衝區: 200MB
──────────────────────────────
可用於專案: ~2500MB(總計 4096MB)
正在運行的專案(截至 2026年6月26日 — 真實名稱請見 gameboy.local.md):
app-workflow(Activepieces):1536MB 限制 / 768MB 保留app-billing(Next.js):512MB 限制 / 256MB 保留app-landing(靜態):256MB 限制 / 128MB 保留- 已佔用: 2304MB 限制 / 1152MB 保留
關卡流程(依此順序執行,不可跳過)
階段 G0:取得伺服器快照
總是先做這一步 — 這是伺服器當前狀態的真實反映:
<GAMEBOY_SSH> "
free -h &&
docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}' &&
df -h / &&
docker inspect \$(docker ps -q) --format '{{.Name}}: mem={{.HostConfig.Memory}}' 2>/dev/null | sort
"
從中推導出:
TOTAL_RAM_MB(幾乎總是 3840MB = 3.7GB 以 bytes 計)COOLIFY_OVERHEAD_MB= 所有 Coolify 自身容器目前總和(coolify、coolify-db、coolify-redis、coolify-sentinel、coolify-proxy)RUNNING_PROJECTS_MB= 所有運行中專案容器的 mem_limit 總和AVAILABLE_MB= TOTAL_RAM_MB - 400 (OS) - COOLIFY_OVERHEAD_MB - RUNNING_PROJECTS_MB - 200 (安全緩衝)
階段 G1:限制計算(AI 驅動,無硬編碼)
根據專案類型,AI 計算出正確的限制:
專案類型識別:
- Node.js / Next.js 應用程式 → BASE_MB = 350,每個 Worker 額外 50
- Python / Django / FastAPI → BASE_MB = 200,每個 Worker 額外 30
- Go / Rust 服務 → BASE_MB = 80, 每個 Worker 額外 20
- 靜態網站 (nginx/caddy) → BASE_MB = 64, 每個 Worker 額外 0
- Postgres → BASE_MB = 128, 每個連線額外 10
- Redis → BASE_MB = 64, 每個 Worker 額外 0
- 背景 Worker (佇列) → BASE_MB = 200, 每個 Worker 額外 50
- AI/LLM 服務(本機) → BASE_MB = 800+ (若模型在本機,至少 2GB)
限制公式:
MEM_LIMIT_MB = min(AVAILABLE_MB * 0.8, BASE_MB * 3) -- 可用量的 80%,最多 3 倍 Base
MEM_RESERVATION = MEM_LIMIT_MB * 0.5 -- 50% 作為保證最小值
MEM_SWAP_MB = MEM_LIMIT_MB * 1.4 -- 允許 40% 的 Swap 開銷
CPU_LIMIT = round(2.0 * (MEM_LIMIT_MB / AVAILABLE_MB), 1) -- 與 RAM 佔比成比例
NODE_OPTIONS_MB = floor(MEM_LIMIT_MB * 0.70) -- V8 堆積 = 容器限制的 70%
UV_THREADPOOL_SIZE = min(8, cpu_count * 2) -- libuv 執行緒數
在設定之前進行驗證檢查:
-
MEM_LIMIT_MB >= 128(啟動所需的最小值) -
AVAILABLE_MB - MEM_LIMIT_MB >= 200(安全緩衝區仍存在) - 所有 mem_limit 總和 ≤ 3500MB(不超過實體 RAM 的 85%)
- 如果驗證失敗 → 封鎖,通知 Cem
階段 G2:在 Coolify DB 中永久設定限制
這是防止部署回歸的關鍵修復。 Coolify 每次部署時都會從自己的資料庫重新產生 docker-compose.yaml。只有當限制存在於資料庫中時,它們才能在部署後持續存在。
如果知道應用程式的 UUID:
<GAMEBOY_SSH> "
docker exec coolify-db psql -U coolify -d coolify -c \"
UPDATE applications SET
limits_memory = '<MEM_LIMIT_MB>m',
limits_memory_swap = '<MEM_SWAP_MB>m',
limits_memory_swappiness = 10,
limits_memory_reservation = '<MEM_RESERVATION>m',
limits_cpus = '<CPU_LIMIT>',
limits_cpu_shares = 1024
WHERE uuid = '<APP_UUID>';
SELECT uuid, name, limits_memory, limits_cpus FROM applications WHERE uuid='<APP_UUID>';
\"
"
從 Coolify URL 取得 UUID:<COOLIFY_URL>/project/.../application/<UUID>/...
如果 UUID 未知 → 透過資料庫搜尋:
docker exec coolify-db psql -U coolify -d coolify -c "SELECT uuid, name FROM applications ORDER BY created_at DESC LIMIT 10;"
階段 G3:設定環境變數
在專案的 .env 中設定(透過 echo >> .env 或 Coolify UI):
# 對於 Node.js/Next.js:
NODE_OPTIONS=--max-old-space-size=<NODE_OPTIONS_MB>
UV_THREADPOOL_SIZE=<UV_THREADPOOL_SIZE>
# 對於工作流程應用程式(Activepieces 等):
AP_MAX_CONCURRENT_JOBS=5 # 防止沙箱爆炸
AP_TRIGGER_DEFAULT_POLL_INTERVAL=15 # 減少資料庫基準負載
# 對於所有應用程式:
# (生產環境中不要使用 DEBUG 日誌!)
NODE_ENV=production
重要:不要有換行錯誤! 檢查每個變數是否在自己的行上:
grep -c '^[A-Z_].*=' .env # 應等於變數數量
# 除了值中,任何行都不能包含兩個 '='
awk -F= 'NF>2 && $1 ~ /^[A-Z_]/' .env && echo "錯誤:行中包含雙重 =!"
階段 G4:啟用 Traefik 速率限制
對於每個新的公開服務:
<GAMEBOY_SSH> "
cat > /data/coolify/proxy/dynamic/rate-limit-global.yaml << 'EOF'
http:
middlewares:
rate-limit-strict:
rateLimit:
average: 60
burst: 120
period: 1s
sourceCriterion:
ipStrategy:
depth: 1
rate-limit-api:
rateLimit:
average: 30
burst: 60
period: 1s
sourceCriterion:
ipStrategy:
depth: 1
EOF
echo '速率限制配置已寫入'
"
Traefik 會自動拾取此配置(watch=true)— 無需重啟。
然後將 Middleware 加入應用程式的路由器(在應用程式特定的 Traefik 配置中):
middlewares:
- rate-limit-strict
階段 G5:設定檔案描述符限制
Docker daemon 必須知道這些限制。每個 Gameboy 設定只需執行一次,之後就會持久存在:
<GAMEBOY_SSH> "
# 檢查是否已設定
grep -q 'nofile' /etc/docker/daemon.json && echo 'already set' || {
# 備份並更新
cp /etc/docker/daemon.json /etc/docker/daemon.json.bak.\$(date +%Y%m%d)
python3 -c \"
import json
with open('/etc/docker/daemon.json') as f: d = json.load(f)
d['default-ulimits'] = {'nofile': {'Name': 'nofile', 'Hard': 65536, 'Soft': 65536}}
with open('/etc/docker/daemon.json', 'w') as f: json.dump(d, f, indent=2)
print('daemon.json updated')
\"
# 注意:systemctl restart docker 會重新啟動所有容器!
# 僅在無活躍流量時進行 → 與 Cem 協調
echo '需要:systemctl restart docker(短暫停機!)'
}
"
階段 G6:壓力測試(真正的入學考試)
50 個並發使用者,持續 60 秒:
# 測試 1:基本可達性
curl -o /dev/null -s -w "HTTP=%{http_code} %{time_total}s\n" https://<APP_URL>/
# 測試 2:50 個並發 HTTP 請求(30 秒)
/usr/sbin/ab -n 3000 -c 50 -t 30 -r -k https://<APP_URL>/ 2>&1 | grep -E "Requests|Failed|Time per|50%|90%|99%"
# 測試 3:在負載期間監控記憶體
<GAMEBOY_SSH> "for i in \$(seq 1 6); do sleep 10; docker stats --no-stream --format '{{.Name}}: {{.MemUsage}} {{.MemPerc}} CPU={{.CPUPerc}}' | grep -v NAMES; echo '---'; done" &
MONITOR_PID=\$!
/usr/sbin/ab -n 5000 -c 50 -t 55 -r https://<APP_URL>/ 2>/dev/null
wait \$MONITOR_PID
階段 G7:PASS/FAIL 標準
PASS(必須滿足所有條件):
| 標準 | 閾值 |
|---|---|
| HTTP 狀態 | 2xx 或 3xx(無 502/503/504) |
| p99 延遲 | < 3000ms |
| 記憶體增長 | 穩定(在 t=0 和 t=60s 之間 < +20%) |
| 記憶體低於限制 | < 容器限制的 90% |
| 無失敗請求 | < 0.1% |
| 測試後伺服器可達 | 所有 URL 返回 HTTP 200/302 |
| 測試後 CPU | < 80% 持續 |
FAIL(只要一個就封鎖):
- 測試後有任何容器宕機或重啟
- 記憶體達到限制(> 95%)
失敗請求 > 1%p99 > 5000ms- Watchdog 在測試期間介入(檢查:
<WATCHDOG_LOG>)
階段 G8:關卡結果
═══════════════════════════════════════════════════════
🎮 GAMEBOY-GATE 結果
═══════════════════════════════════════════════════════
專案: [名稱]
App-UUID: [Coolify UUID]
測試時間: [日期/時間]
限制計算:
伺服器 RAM: 3840MB
Coolify 開銷: [n]MB(已測量)
其他專案: [n]MB(限制總和)
可用: [n]MB
已分配: MEM=[n]m,CPU=[n],NODE_OPTIONS=[n]MB
檢查:
✅/❌ Coolify DB 限制已持久設定
✅/❌ NODE_OPTIONS 正確(無換行錯誤)
✅/❌ 速率限制已啟用
✅/❌ 檔案描述符限制 65536
✅/❌ Redis maxmemory 策略已設定(如果是 Redis)
✅/❌ 壓力測試 50c/30s 通過
壓力測試:
請求/秒: [n] (PASS 當 > 10)
p50: [n]ms
p99: [n]ms (PASS 當 < 3000ms)
失敗: [n]% (PASS 當 < 0.1%)
記憶體峰值: [n]MB (PASS 當 < 限制的 90%)
═══════════════════════════════════════════════════════
判決: ✅ PASS — 允許發佈
❌ FAIL — 封鎖發佈([原因])
═══════════════════════════════════════════════════════
常見 FAIL 原因與修復
FAIL:NODE_OPTIONS 未設定
# 檢查:
docker exec <container> printenv NODE_OPTIONS
# 修復:
grep -n 'NODE_OPTIONS' .env # 檢查換行錯誤
# 錯誤:HOST=0.0.0.0NODE_OPTIONS=... → 修復:
sed -i 's/HOST=0.0.0.0NODE_OPTIONS=/HOST=0.0.0.0\nNODE_OPTIONS=/' .env
FAIL:測試後容器 OOM
根本原因幾乎總是以下之一:
NODE_OPTIONS未設定 → V8 完全佔用容器 RAM- 沙箱進程(AP、程式碼執行)沒有併發限制
- 應用程式碼中的記憶體洩漏(較少見)
# 檢查是否發生重啟:
docker inspect <container> --format '{{.RestartCount}}'
# 檢查是否發生 OOM 殺死:
dmesg | grep -i 'oom\|killed' | tail -10
FAIL:p99 > 3000ms
- Heavy API 端點沒有快取 → 實作 Redis 快取
- PG 連線池耗盡 → 增加
MAX_CONCURRENT_REQUESTS或池大小 - 每個請求的 Forward-Auth 開銷 → 加入 JWT 快取
FAIL:失敗請求 > 0.1%
- Traefik 超時短於應用程式回應 → 增加 Traefik 超時
- FD 限制耗盡 → 步驟 G5 (ulimits)
- PG max_connections 耗盡 → 在 PG 中設定
max_connections=200
部署彈性:部署存活協議
每次 Coolify 部署都必須保證以下幾點,以防止伺服器崩潰:
每次部署前檢查:
<GAMEBOY_SSH> "
# 可用 RAM 必須 > 800MB
FREE=\$(free -m | awk '/^Mem:/{print \$7}')
echo "可用:\${FREE}MB"
[ \$FREE -lt 800 ] && echo '警告:部署 RAM 不足!' || echo 'RAM 部署 OK'
# 磁碟必須 > 5GB 可用(拉取映像需要空間)
DISK=\$(df / | awk 'NR==2{print \$4}')
echo "磁碟可用:\$((DISK/1024))MB"
[ \$DISK -lt 5242880 ] && echo '警告:磁碟空間不足!' || echo '磁碟 OK'
"
Coolify 在部署時做了什麼(以及我們需要確保什麼):
- Coolify 讀取資料庫 → 產生 docker-compose.yaml 帶有在資料庫中設定的限制(G2 → 持久 ✅)
- 容器停止 → 短暫停機
- 新容器啟動 → 健康檢查等待應用程式就緒
- Traefik 取得新的後端端點
健康檢查配置(在 Coolify UI 或直接在資料庫中):
docker exec coolify-db psql -U coolify -d coolify -c "
UPDATE applications SET
health_check_enabled = true,
health_check_path = '/api/v1/flags', -- 或 '/', '/health', '/ping'
health_check_port = '80',
health_check_interval = 15,
health_check_timeout = 5,
health_check_retries = 3,
health_check_start_period = 90 -- 重型應用程式 90 秒啟動時間
WHERE uuid = '<APP_UUID>';
"
記憶體預算表(截至 2026年6月26日)
GAMEBOY 4GB RAM — 預算概覽
════════════════════════════════════════════════════════════
系統/OS: 400MB (固定)
Coolify 堆疊: ~950MB (coolify+db+redis+sentinel+proxy)
────────────────────────────────────────────────────────────
可用於專案: ~2650MB
════════════════════════════════════════════════════════════
app-workflow: 1536MB 限制 / 768MB 保留
app-billing: 512MB 限制 / 256MB 保留
app-landing: 256MB 限制 / 128MB 保留
────────────────────────────────────────────────────────────
仍可用: ~346MB 限制空間
════════════════════════════════════════════════════════════
警告:Gameboy 的限制容量已佔用約 91%。
超過 300MB 的新專案需要重新平衡(縮小現有專案
或最佳化 Coolify 堆疊)。
如果新專案需要超過 200MB → 先檢查是否可以將 landing(256MB)減少到 128MB(靜態網站,只需要很少的 RAM)。
每次部署的檢查清單(快速參考)
部署前:
[ ] 可用 RAM > 800MB
[ ] 磁碟 > 5GB 可用
[ ] Coolify DB 限制已設定(非 0!)
[ ] .env 中的 NODE_OPTIONS 正確(無換行錯誤)
[ ] 已為公開端點配置速率限制
部署後:
[ ] 容器正在執行(docker ps → Up X seconds (healthy))
[ ] URL 可達(HTTP 2xx/3xx)
[ ] NODE_OPTIONS 生效(docker exec <c> printenv NODE_OPTIONS)
[ ] 記憶體穩定(docker stats → 未達限制)
[ ] 無 OOM 殺死(dmesg | grep -i oom | tail -5)
設定檔與路徑
所有技能都位於:~/.claude/skills/
每個技能一個目錄。載入順序:IPO(Letter),例如此技能來自 ~/.claude/skills/01-gameboy-gate/SKILL.md