🎮 Gameboy-Gate — 每个项目的入学考试
必须: 部署到 Gameboy(
<GAMEBOY_IP>)上的每个项目都必须通过此门禁——无论谁在使用 Claude,无论什么项目。没有通过则禁止发布。
🔒 连接数据本地化:
<GAMEBOY_IP>、<GAMEBOY_SSH>、<COOLIFY_URL>、<WATCHDOG_*>的实际值存储在 gitignore 的gameboy-gate/gameboy.local.md文件中(不在公开仓库中)。每次运行门禁前,读取此文件并替换占位符。
上下文: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
操作系统:Ubuntu (Docker + Coolify)
Coolify UI:<COOLIFY_URL> # 实际值:见 gameboy.local.md
固定开销预留(永不低于):
操作系统 + 内核: 400MB
Coolify(含数据库、Redis、代理、哨兵):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 字节)COOLIFY_OVERHEAD_MB= 所有 Coolify 自身容器当前的总和(coolify、coolify-db、coolify-redis、coolify-sentinel、coolify-proxy)RUNNING_PROJECTS_MB= 所有运行中项目容器的 mem_limit 总和AVAILABLE_MB= TOTAL_RAM_MB - 400(操作系统)- COOLIFY_OVERHEAD_MB - RUNNING_PROJECTS_MB - 200(安全缓冲区)
阶段 G1:限制计算(AI 驱动,无硬编码)
根据项目类型,AI 计算正确的限制:
识别项目类型:
- Node.js / Next.js 应用 → BASE_MB = 350,每个 Worker 的 WORKER_OVERHEAD = 50
- Python / Django / FastAPI → BASE_MB = 200,每个 Worker 的 WORKER_OVERHEAD = 30
- Go / Rust 服务 → BASE_MB = 80,每个 Worker 的 WORKER_OVERHEAD = 20
- 静态站点 (nginx/caddy) → BASE_MB = 64,WORKER_OVERHEAD = 0
- Postgres → BASE_MB = 128,每个连接的 WORKER_OVERHEAD = 10
- Redis → BASE_MB = 64,WORKER_OVERHEAD = 0
- 后台 Worker (队列) → BASE_MB = 200,每个 Worker 的 WORKER_OVERHEAD = 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% 交换开销
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_limits 总和 ≤ 3500MB(不超过物理内存的 85% 过度分配)
- 如果验证失败 → 阻止,通知 Cem
阶段 G2:在 Coolify 数据库中永久设置限制
这是防止部署回归的关键修复。 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)——无需重启。
然后将中间件添加到应用路由器(在应用特定的 Traefik 配置中):
middlewares:
- rate-limit-strict
阶段 G5:设置文件描述符限制
Docker 守护进程必须知道限制。每个 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 已更新')
\"
# 注意: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:通过/失败标准
通过(必须全部满足):
| 标准 | 阈值 |
|---|---|
| HTTP 状态 | 2xx 或 3xx(无 502/503/504) |
| p99 延迟 | < 3000ms |
| 内存增长 | 稳定(t=0 到 t=60s 之间 < +20%) |
| 内存低于限制 | < 容器限制的 90% |
| 无失败请求 | < 0.1% |
| 测试后服务器可达 | 所有 URL 返回 HTTP 200/302 |
| 测试后 CPU | < 80% 持续 |
失败(任一条件即阻止):
- 测试后任何容器关闭/重启
- 内存达到限制(> 95%)
- 失败请求 > 1%
- p99 > 5000ms
- 看门狗在测试期间介入(检查:
<WATCHDOG_LOG>)
阶段 G8:门禁结果
═══════════════════════════════════════════════════════
🎮 GAMEBOY-GATE 结果
═══════════════════════════════════════════════════════
项目: [名称]
应用 UUID:[Coolify UUID]
测试时间:[日期/时间]
限制计算:
服务器 RAM: 3840MB
Coolify 开销: [n]MB(实测)
其他项目: [n]MB(限制总和)
可用: [n]MB
已分配: 内存=[n]m,CPU=[n],NODE_OPTIONS=[n]MB
检查:
✅/❌ Coolify 数据库限制已持久设置
✅/❌ NODE_OPTIONS 正确(无换行错误)
✅/❌ 速率限制已启用
✅/❌ 文件描述符限制 65536
✅/❌ Redis maxmemory 策略已设置(如果是 Redis)
✅/❌ 负载测试 50c/30s 通过
负载测试:
请求/秒:[n] (通过要求 > 10)
p50: [n]ms
p99: [n]ms (通过要求 < 3000ms)
失败: [n]% (通过要求 < 0.1%)
内存峰值:[n]MB (通过要求 < 限制的 90%)
═══════════════════════════════════════════════════════
判决:✅ 通过 — 允许发布
❌ 失败 — 发布被阻止([原因])
═══════════════════════════════════════════════════════
常见失败原因及修复
失败: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
失败:测试后容器 OOM
根本原因几乎总是以下之一:
NODE_OPTIONS未设置 → V8 占用全部容器内存- 沙盒进程(AP、代码执行)没有并发限制
- 应用代码中的内存泄漏(较少见)
# 检查是否发生重启:
docker inspect <container> --format '{{.RestartCount}}'
# 检查是否发生 OOM 杀死:
dmesg | grep -i 'oom\|killed' | tail -10
失败:p99 > 3000ms
- 没有缓存的重量级 API 端点 → 实现 Redis 缓存
- PG 连接池耗尽 → 增加
MAX_CONCURRENT_REQUESTS或池大小 - 每个请求的前向认证开销 → 实现 JWT 缓存
失败:失败请求 > 0.1%
- Traefik 超时短于应用响应 → 增加 Traefik 超时
- 文件描述符限制耗尽 → 步骤 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 '警告:部署内存不足!' || echo 'RAM 适合部署'
# 磁盘必须 > 5GB 空闲(拉取镜像需要空间)
DISK=\$(df / | awk 'NR==2{print \$4}')
echo "磁盘空闲:\$((DISK/1024))MB"
[ \$DISK -lt 5242880 ] && echo '警告:磁盘空间不足!' || echo '磁盘 OK'
"
Coolify 在部署时做什么(以及我们必须确保什么):
- Coolify 读取其数据库 → 生成 docker-compose.yaml,包含数据库中设置的限制(G2 → 持久 ✅)
- 容器停止 → 短暂停机
- 新容器启动 → Healthcheck 等待应用就绪
- Traefik 获取新的后端端点
Healthcheck 配置(在 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 — 预算概览
════════════════════════════════════════════════════════════
系统/操作系统: 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 > 800MB
[ ] 磁盘 > 5GB 空闲
[ ] Coolify 数据库限制已设置(不为 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 …