🎮 Gameboy-Gate — 全プロジェクトの入学試験
必須: Gameboy(
<GAMEBOY_IP>)にデプロイされるすべてのプロジェクトは、このゲートを通過する必要があります。Claudeを使用するユーザーやプロジェクトの種類を問いません。PASSなしにリリースはできません。
🔒 接続情報はローカル:
<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
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(Static): 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 (OS) - COOLIFY_OVERHEAD_MB - RUNNING_PROJECTS_MB - 200 (セーフティ)
フェーズ G1: 制限計算(AI主導、ハードコードなし)
プロジェクトタイプに基づいて、AIが適切な制限を計算します:
プロジェクトタイプの識別:
- Node.js / Next.js アプリ → BASE_MB = 350, WORKER_OVERHEAD = ワーカーあたり 50
- Python / Django / FastAPI → BASE_MB = 200, WORKER_OVERHEAD = ワーカーあたり 30
- Go / Rust サービス → BASE_MB = 80, 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
- バックグラウンドワーカー (キュー) → BASE_MB = 200, WORKER_OVERHEAD = ワーカーあたり 50
- AI/LLM サービス (ローカル) → BASE_MB = 800+ (モデルがローカルの場合は最低 2GB)
制限の計算式:
MEM_LIMIT_MB = min(AVAILABLE_MB * 0.8, BASE_MB * 3) -- 利用可能量の80%、最大ベースの3倍
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_limit の合計 ≤ 3500MB (物理RAMの85%を超えるオーバーコミットなし)
- 検証に失敗した場合 → ブロック、Cemに通知
フェーズ G2: Coolify DBに制限を永続的に設定
これはデプロイ後退行を防ぐための重要な修正です。 Coolifyはデプロイのたびに自身のDBから docker-compose.yaml を再生成します。制限がDBに設定されている場合にのみ、デプロイ後も持続します。
アプリの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>';
\"
"
UUIDはCoolify URLから取得: <COOLIFY_URL>/project/.../application/<UUID>/...
UUIDが不明な場合 → DBで検索:
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 # DBベースライン負荷を低減
# すべてのアプリで:
# (本番環境ではDEBUGログ出力なし!)
NODE_ENV=production
重要: 改行バグに注意! 各変数が個別の行にあることを確認:
grep -c '^[A-Z_].*=' .env # 変数の数と一致する必要あり
# 値に含まれる場合を除き、1行に2つの '=' があってはならない
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セットアップごとに1回、永続的に保持されます:
<GAMEBOY_SSH> "
# 既に設定されているか確認
grep -q 'nofile' /etc/docker/daemon.json && echo 'すでに設定されています' || {
# バックアップと更新
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: 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(1つでブロック):
- テスト後にダウン/再起動したコンテナがある
- メモリが制限に達した (> 95%)
Failed requests > 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 をパス
負荷テスト:
Req/s: [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
- キャッシュなしのヘビー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は十分です'
# ディスク空き容量が5GB以上必要 (イメージプルにスペースが必要)
DISK=\$(df / | awk 'NR==2{print \$4}')
echo "ディスク空き: \$((DISK/1024))MB"
[ \$DISK -lt 5242880 ] && echo '警告: ディスクが不足しています!' || echo 'ディスクは十分です'
"
Coolifyがデプロイ時に行うこと(そして私たちが保証しなければならないこと):
- Coolifyは自身のDBを読み取る → DBに設定された制限を含めて docker-compose.yaml を生成 (G2 → 永続的 ✅)
- コンテナ停止 → 短いダウンタイム
- 新しいコンテナ起動 → Healthcheckはアプリの準備ができるまで待機
- Traefikは新しいバックエンドエンドポイントを取得
Healthcheck設定 (Coolify UI または直接DB):
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\|killed')
[ ] 負荷テスト完了 (50c/60s)
最終ルール: 疑わしい場合は、常にゲートを実行してください。Gameboyにリソースが不足しているよりも、デプロイに少し時間がかかる方がはるかに良いです。