# EwoooC 運維與自動化手冊 (DevOps Handbook)

## 🌐 伺服器連線
| 環境 | 主機 | 指令 |
|---|-|---|
| **正式 (EwoooC)** | 192.168.0.188 | `ssh -J wooo@192.168.0.110 ollama@192.168.0.188` |
| **周邊 (UAT)** | 192.168.0.110 | `ssh wooo@192.168.0.110` |
| **安全 (Kali)** | 192.168.0.112 | `ssh kali@192.168.0.112` |

---

## 🐋 Docker & Docker Compose 指令 (188 主機)
執行路徑：`/home/ollama/momo-pro`

### 常用操作
- **查看狀態**: `docker ps | grep momo`
- **查看日誌**: `docker logs -f momo-pro-system --tail 100`
- **熱重載主應用**: `docker kill -s HUP momo-pro-system`
- **精準重建應用容器**: `docker compose up -d --no-deps --force-recreate momo-app`
- **精準重建排程/Telegram**: `docker compose up -d --no-deps --force-recreate scheduler telegram-bot`
- **全面啟動**: `docker compose up -d`
- **進入資料庫**: `docker exec -it momo-db psql -U momo`

紅線：
- 禁止使用 `docker compose down` 或 `--remove-orphans`。
- 禁止 stop/remove/recreate `momo-db`；資料庫生命週期需另走明確維護流程。

### 影像管理
- **重建影像**: `docker compose build --no-cache momo-app`
- **清理過期資源**: `docker system prune -f`

---

## 🌐 Nginx 反向代理 (110 主機)
配置路徑：`/etc/nginx/sites-enabled/momo`

- **重新載入**: `sudo systemctl reload nginx`
- **語法檢查**: `sudo nginx -t`

---

## 🤖 n8n 工作流程管理 (188 主機)
- **進入容器**: `docker exec -it n8n /bin/sh`
- **手動執行 Workflow**: `docker exec n8n n8n execute --id=<workflow_id>`
- **列出 Workflow**: `docker exec n8n n8n list:workflow`

---

## ❄️ K8s / K3s 狀態
已撤除。EwoooC 正式 runtime 是 188 主機的 Docker Compose，110 只作為 Gateway / Nginx / Gitea 等周邊服務主機。

若看到舊文件提到 Pod、Deployment、PVC 或叢集操作，先以 `docs/adr/ADR-008-actual-runtime-on-188.md` 與 `docs/adr/ADR-011-cross-project-resource-isolation.md` 為準，不要照抄舊叢集命令。

---

## 🏥 健康檢查與自動修復
**監控腳本**: `/home/wooo/scripts/domain-health-monitor.sh` (於 110 執行)
**頻率**: 每 5 分鐘
- `https://mo.wooo.work/health` -> 200
- `https://monitor.wooo.work/` -> 200
- `https://registry.wooo.work/v2/` -> 401

---

## 🆘 故障排除 (Troubleshooting) - 2026-04-28 實戰總結

### 0. 110 → 188 SCP 報 `Host key verification failed`
- **原因**: 110 的 `~/.ssh/known_hosts` 保留了 188 的舊 host key 或缺少目前 key，導致部署檔案傳輸被 SSH 安全檢查擋下。
- **修復**: 在 110 執行 `ssh-keygen -R 192.168.0.188 && ssh-keyscan -H 192.168.0.188 >> ~/.ssh/known_hosts`。
- **驗證**: 先把 `/tmp/momo_scp_smoke.txt` 從 110 傳到 188 的 `/tmp`，再用 `ssh ollama@192.168.0.188 'cat /tmp/momo_scp_smoke.txt'` 確認可讀，最後刪除 smoke 檔。
- **紅線**: 不要把正式部署指令改成長期 `StrictHostKeyChecking=no`；遇到 key 問題要修 known_hosts，而不是關閉驗證。

### 1. 網站 502 Bad Gateway (Nginx 找不到後端)
- **原因**: 110 與 188 之間的 SSH 隧道中斷。
- **檢查**: 在 110 執行 `curl -I http://127.0.0.1:5003/health`。
- **修復**: 在 110 執行 `ssh -fN -L 5003:127.0.0.1:5003 ollama@192.168.0.188` 重啟隧道。
- **CD 判斷**: 先確認 188 內部 `docker exec momo-pro-system curl http://127.0.0.1:80/health`，再看外部 `https://mo.wooo.work/health`；若 internal 已 200 但 external 502，多半是 Nginx/tunnel 短暫延遲。

### 2. CI/CD 報錯 `parent snapshot ... not found`
- **原因**: Docker Buildx 快取損壞。
- **修復**: 在 `.gitea/workflows/cd.yaml` 中使用 `docker compose build --no-cache`。

### 3. CI/CD 報錯 `No such container: momo-pro-system`
- **原因**: Sync 模式使用硬編碼容器名，但容器可能已被重建或改名。
- **修復**: 先 `docker compose up -d --no-deps momo-app scheduler telegram-bot` 確保容器存在，再 `docker compose restart momo-app scheduler telegram-bot` 讓 bind-mounted Python 檔案重新載入。

### 4. Telegram Bot 代碼更新無效 (幽靈容器)
- **原因**: `docker-compose.yml` 遺漏了 `/app/routes` 的 Volume 掛載。
- **檢查**: `docker inspect momo-telegram-bot | jq '.[0].Mounts'`。
- **修復**: 確保 `volumes` 段落包含 `- ./routes:/app/routes:ro`。

### 5. Gunicorn 設定更新後仍吃舊版 image 內容
- **原因**: `gunicorn.conf.py` 若沒有 bind mount，容器 restart 會使用 image 內建檔案；host 上新版設定不會自動進入 `/app/gunicorn.conf.py`。
- **檢查**: `docker inspect momo-pro-system | jq '.[0].Mounts'`，確認有 `/app/gunicorn.conf.py`。
- **修復**: `docker-compose.yml` 的 `momo-app.volumes` 必須包含 `- ./gunicorn.conf.py:/app/gunicorn.conf.py:ro`，再走 CD rebuild 或精準 recreate app/scheduler/bot。

### 6. `/metrics` 持續出現 realtime_sales_monthly 欄位不存在 warning
- **原因**: 線上表 schema 可能比 ORM 窄，ORM count 會包子查詢並引用不存在欄位。
- **修復**: metrics 只需要總筆數時使用 `SELECT COUNT(*) FROM realtime_sales_monthly`，不要透過 `session.query(RealtimeSalesMonthly).count()`。

### 7. Rebuild 模式造成長時間 502
- **原因**: CD 若先 `docker stop/rm` 再 `docker compose build --no-cache`，build 耗時會全部變成服務中斷時間。
- **修復**: rebuild 模式必須先 `docker compose build --no-cache momo-app` 成功，才短暫 stop/rm/recreate `momo-app scheduler telegram-bot`。
- **救急**: 若 build 已經在跑且三應用容器已被移除，可精準執行 `docker compose up -d --no-deps momo-app scheduler telegram-bot` 先恢復服務；禁止使用 `--remove-orphans`。

### 8. ElephantAlpha NIM 回 404
- **原因**: NVIDIA `/v1/models` 可能列出模型，但帳號未必可呼叫該 hosted function；production 曾對 Ultra 253B 回 `Function ... Not found for account`。
- **檢查**: 在容器內用 `NVIDIA_API_KEY` 呼叫 `https://integrate.api.nvidia.com/v1/models`，再用低 `max_tokens` 測試目標 model。
- **修復**: `ELEPHANT_ALPHA_MODEL` 預設使用 `nvidia/llama-3.3-nemotron-super-49b-v1.5`，並保留 `ELEPHANT_ALPHA_FALLBACK_MODELS`。

### 9. PostgreSQL 連線數快速升高
- **原因**: Flask routes 會頻繁建立 `DatabaseManager()`，若每次都產生新 engine/pool，Gunicorn 多 worker 會快速吃滿 PostgreSQL clients。
- **修復**: `DatabaseManager` 以 `(DATABASE_TYPE, DATABASE_PATH)` 重用 engine/session，且 PostgreSQL pool 收斂為 `pool_size=2/max_overflow=3`。
- **檢查**: app log 應出現 `使用 PostgreSQL 資料庫 (連線池已收斂)`，Gunicorn `post_fork` 仍需 dispose inherited engines。

### 10. OpenClaw embedding timeout
- **原因**: Hermes/Ollama 負載高或舊 `/api/embeddings` endpoint 慢，會讓 embedding worker 累加 retry。
- **檢查**: 看 `embedding_retry_queue` 的 `pending/processing/failed` 分布，並測 `http://192.168.0.111:11434/api/embed`。
- **修復**: client 預設使用官方 `/api/embed`；若舊節點 404/405 才 fallback `/api/embeddings`。必要時調整 `EMBEDDING_TIMEOUT`。

### 11. `/health` 偶發 000 或容器 unhealthy，但 app log 仍有 200
- **原因**: Blackbox 或外部探測打 Dashboard 首頁 `/`，會觸發商品看板與 PChome 比價重型查詢；少量 sync worker 被長請求佔滿時，輕量 `/health` 也會排隊逾時。
- **檢查**: `docker logs momo-pro-system --since 20m | grep 'Blackbox-Exporter'` 應只看到 `GET /health`；`docker stats momo-db` 若接近多核心滿載，需同步看 `pg_stat_activity` 的 `latest_momo` 類查詢。
- **修復**: 188 的 `monitoring/prometheus.yml` 與 110 的 `/home/wooo/monitoring/prometheus.yml` blackbox HTTP targets 必須使用 `/health`；Gunicorn 保持 `worker_class=gthread`、`GUNICORN_THREADS=4`、`preload_app=False`。

### 12. GCP-A Ollama refused / 110:11435 502
- **快速診斷**: 在 repo 根目錄執行 `scripts/ops/diagnose_ollama_gcp_failover.sh`。此腳本不需要 root，也不會修改 nginx、Docker、GCP 或正式服務。
- **判讀**:
  - `GCP-A direct /api/version` 失敗且 `GCP-B direct` 成功：primary VM、防火牆或 Ollama 服務異常；應用層會走 GCP-A → GCP-B → 111，但仍需修 primary。
  - `110 proxy primary` 502 且 `110 proxy secondary` 成功：110 的 `11435` 固定代理 GCP-A，所以 primary 掛時舊 proxy 入口會失敗；需 110 root 才能改 nginx 或 reload。
  - `GCP-B embed` 成功但耗時接近 30s：表示 `bge-m3` runner 慢但可用；若經常超過 30s，應處理 GCP-B runner/CPU/模型併發，不要把 111 納入背景 embedding。
- **GCP-A 修復方向**:
  - 有 GCP/SSH 權限時，先確認 VM 是否開機、Firewall 是否開 `22` 與 `11434`、Ollama process 是否在 listen。
  - 110 現況若 `ssh gcp-a` 回 `port 22: Connection refused`，代表目前跳板無法進主機，不能靠 momo-pro app 修復。
- **110 proxy failover 方向**:
  - 需要 root：`sudo nginx -t`、修改 `/etc/nginx/sites-enabled/110-ollama-proxy.conf`、`sudo systemctl reload nginx`。
  - 若要讓 `11435` 在 GCP-A 掛時 fallback 到 GCP-B，必須明確標註這是 proxy failover，不代表 GCP-A 已恢復；host health 仍應以 direct GCP-A 探針為準。
- **紅線**:
  - 不要把背景 `bge-m3` 任務改落 111。
  - 不要用更長 timeout 掩蓋 GCP-A refused；GCP-A 是 primary infra blocker。
  - 沒有 110 root 或 GCP SSH 權限時，只能完成診斷、文件與應用層降級，不能假裝已修復 primary。
