commit 6a7aaba059ee27da0a4c87fa5a26ff012f1e0f72 Author: Timmy Date: Sun Jun 21 21:54:13 2026 +0800 CT124 故障復原報告與 CLAUDE.md Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ec79c0f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,29 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +This is **not a software project** — there is no code, build, or test. It is an operations/infrastructure +notes directory for a self-hosted homelab running **Docker Compose stacks inside Proxmox LXC containers**. +Files are post-incident / runbook documents written in Traditional Chinese. + +Current contents: `CT124_故障復原_20260529.md` — a recovery report for a full Docker/Tailscale outage on host +CT124 (192.168.42.124, Ubuntu 24.04 LXC) after a hard power-off. + +## Working in here + +- Documents are the deliverable. "Editing" means writing/revising Markdown, not changing code. +- Filename convention: `__.md` (e.g. `CT124_故障復原_20260529.md`). +- Shell snippets in the docs run on the LXC host as root; they are records of what was done, not scripts to + execute here. Don't run them against any machine unless explicitly asked. +- Cross-references point to docs **not in this directory** (e.g. `PVECluster_Architecture.md`'s "停電復電 SOP"). + If a referenced doc is needed, ask — don't assume it exists locally. + +## Key non-obvious fact (CT124) + +The outage's original symptom (Tailscale/Headscale offline) was a downstream effect, not the root cause. The +dependency chain is: **docker daemon → Nginx Proxy Manager container (reverse-proxies :443) → Headscale**. +External `headscale.lotimmy.com:443` hairpins back to CT124's own WAN IP. When Docker dies, NPM dies, port 443 +stops listening, and the Tailscale client logs out. When debugging Tailscale disconnects on this host, verify +this chain first before suspecting Tailscale itself. diff --git a/CT124_故障復原_20260529.md b/CT124_故障復原_20260529.md new file mode 100644 index 0000000..69c5a45 --- /dev/null +++ b/CT124_故障復原_20260529.md @@ -0,0 +1,150 @@ +# CT124 故障復原報告 — Docker / Tailscale 全面當機 + +> 日期:2026-05-29 | 主機:CT124(192.168.42.124,Ubuntu 24.04 LXC) +> 誘因:**非正常關機(硬斷電)** | 結果:38/38 容器恢復,零資料遺失 + +--- + +## 一、摘要 + +CT124 在一次非正常關機(reboot 後 uptime 僅 6 分鐘)後,Docker daemon 陷入 crash-loop,連帶導致這台上約 30 個 compose stack 全數停擺,其中包含對外提供 Headscale 反向代理的 Nginx Proxy Manager(NPM)。最初的症狀是 **Tailscale(Headscale client)連不上 control server**,往下追才發現是整條相依鏈崩潰。 + +最終全部修復,且 Orthanc PACS 的醫療影像 SQLite 索引損毀也以非破壞性方式完整復原(零資料遺失)。 + +| 項目 | 修復後狀態 | +|---|---| +| 容器 | 38 / 38 running,0 not-running | +| docker.service | active | +| Tailscale | `100.64.0.28`,已登入 Headscale | +| Orthanc PACS | 11 patients / 18 studies / 7616 instances / 3.2GB,資料完整 | + +--- + +## 二、根因與相依鏈 + +這是一次 reboot 引發的**連鎖故障**,共六層: + +| # | 問題 | 根因 | +|---|---|---| +| 1 | Docker daemon crash-loop(exit 2) | libnetwork 的 `local-kv.db`(bbolt)損毀,dockerd 還原網路 sandbox 時 panic:`assertion failed: write: circular dependency occurred` | +| 2 | **Tailscale 連不上 Headscale**(原始症狀) | 對外 443 由 CT124 上的 **NPM 容器**反代到 Headscale;NPM 隨 docker 崩潰掛掉,443 無人監聽(`connection refused`) | +| 3 | 約 30 個 compose stack 全停 | reboot + docker 崩潰,非 host-network 的容器全部 Exited(只有 host-network 的 tailscale/netbird + jellyfin 活著) | +| 4 | 後半 stack 重建時 `all predefined address pools have been fully subnetted` | 移除 `local-kv.db` 後,kernel 殘留 21 個孤兒 `br-*` bridge,仍佔著 `172.18–31.x` 與 `192.168.0/48/.../128` 子網,Docker 判定位址池全被佔用 | +| 5 | prometheus crash-loop | 資料目錄 `/opt/ai-proxy-hub/prometheus_data` 屬 root,但容器以 `nobody`(65534) 執行 → `permission denied`(pre-existing,非本次新問題) | +| 6 | orthanc-pacs crash-loop | SQLite 索引 `/opt/ohif-docker/orthanc-db/index` 損毀(`SQLITE_CORRUPT`,code 11),硬斷電寫入中斷所致 | + +### 關鍵非顯性拓撲 + +``` +外部 → headscale.lotimmy.com:443 + │ (DNS → 125.229.110.50 = CT124 自己的 WAN IP,hairpin 繞回內網) + ▼ + CT124:Nginx Proxy Manager 容器 (443 反代) + │ + ▼ + Headscale 本體(內網監聽 :8080,/health 回 {"status":"pass"}) + ▲ + │ /opt/tailscale 容器 (network_mode: host) 作為 client 連回 +``` + +> 相依鏈:**docker daemon → NPM 容器 → Headscale**。NPM 一掛,Tailscale 就 logged out。排查 Tailscale 斷線時務必先確認這條鏈,別誤判成 Tailscale 本身問題。 + +--- + +## 三、修復步驟(依序) + +### 1. 修復 Docker daemon(移除損毀的網路 DB) +```bash +systemctl stop docker.socket docker.service +pkill -9 docker-proxy; pkill -9 dockerd +mv /var/lib/docker/network/files/local-kv.db \ + /var/lib/docker/network/files/local-kv.db.bak.$(date +%s) +systemctl reset-failed docker.service +systemctl start docker.service +``` +> host-network 的 tailscale/netbird/jellyfin 會自動回來;自訂網路的容器仍會 Exited(見步驟 3、4)。 + +### 2. 重建 NPM(恢復對外 443 → Tailscale 即可連線) +```bash +cd /opt/nginx-proxy-manager +docker compose down # 清掉殘留 container 對已死網路 ID 的參照 +docker rm -f nginxproxymanager +docker compose up -d +# 驗證:curl -m10 https://headscale.lotimmy.com/health → HTTP 200 {"status":"pass"} +docker restart tailscale # 讓 client 重新登入 +docker exec tailscale tailscale status # 應取得 100.64.0.x +``` + +### 3. 清除孤兒 bridge(釋放 Docker 位址池) +移除 `local-kv.db` 後,kernel 殘留的 `br-*` 介面不屬於任何現存 docker 網路,卻佔住子網。只刪「非當前網路」的 bridge: +```bash +keep="docker0" +for id in $(docker network ls -q); do keep="$keep br-$id"; done +for br in $(ip -o link show | awk -F': ' '{print $2}' | sed 's/@.*//' | grep '^br-'); do + echo "$keep" | grep -qw "$br" || { ip link set "$br" down; ip link delete "$br"; } +done +``` +> 本次刪除 21 個孤兒 bridge。 + +### 4. 重建所有停掉的 compose stack +```bash +for d in /opt/; do + (cd "$d" && docker compose down >/dev/null 2>&1; docker compose up -d) +done +``` +> 注意:偵測 compose 檔不要用 `ls a.yml b.yml`(多重 glob 只要一個沒中就回非零會誤判);直接 `cd` 進目錄讓 `docker compose` 自動辨識 `docker-compose.yml` / `compose.yaml`。 + +### 5. 修 prometheus 權限 +```bash +chown -R 65534:65534 /opt/ai-proxy-hub/prometheus_data +docker restart prometheus +``` + +### 6. 復原 Orthanc 損毀的 SQLite 索引(非破壞性) +實體影像(`00`–`ff` content-addressed 目錄)完好,只有索引檔損毀。`PRAGMA integrity_check` 顯示為孤立 page + 重複 page 引用: +```bash +cd /opt/ohif-docker/orthanc-db +docker stop orthanc-pacs +cp -a index index.corrupt.bak.$(date +%s) # 先備份 +apt-get install -y sqlite3 # 需 3.45+(Ubuntu 24.04 內建) +sqlite3 index ".dump" > dump.sql # .recover 在此環境 dbpage vtab 失效,改用 .dump +# .dump 因損毀 page 結尾會是 "ROLLBACK; -- due to errors",但資料其實都已 dump 出來: +sed -i 's/^ROLLBACK; -- due to errors/COMMIT;/' dump.sql +sqlite3 index.rebuilt < dump.sql +sqlite3 index.rebuilt "PRAGMA integrity_check;" # → ok +# 比對列數確認零遺失後換上: +mv index index.corrupt.old; mv index.rebuilt index; chown root:root index +docker start orthanc-pacs +``` +復原前後列數比對(完全吻合): + +| 表 | 原損毀檔 | 重建後 | +|---|---|---| +| Resources | 7775 | 7775 | +| Metadata | 83382 | 83382 | +| AttachedFiles(實體影像)| 7746 | 7746 | +| MainDicomTags | 50834 | 50834 | +| Changes | 8124 | 8124 | +| DicomIdentifiers | 7897 | 7897 | +| GlobalProperties | 4(其中 1 列為損毀讀不出)| 3(乾淨,含 `DatabaseSchemaVersion=6`)| + +> 索引檔從 23.5MB 縮為 11.6MB(損毀膨脹被清除)。 + +--- + +## 四、留下的備份(確認運作正常後可刪) + +``` +/var/lib/docker/network/files/local-kv.db.bak. 損毀的 docker 網路 DB +/opt/ohif-docker/orthanc-db/index.corrupt.bak. 損毀的 orthanc 索引 +/opt/ohif-docker/orthanc-db/index.corrupt.old 同上(另一份) +``` + +--- + +## 五、預防建議 + +1. **務必走正常 shutdown**:本次 1(docker 網路 DB)與 6(orthanc SQLite)兩處損毀都源於硬斷電。LXC 裡的 Docker 對非正常關機特別脆弱。可參考 `PVECluster_Architecture.md` 的「停電復電 SOP」。 +2. **UPS / 自動關機**:若停電風險高,建議掛 UPS 並設定 NUT 在斷電時對 CT124 觸發 graceful shutdown。 +3. **Orthanc 改用 PostgreSQL 後端**(選配):SQLite 在 LXC + 突發斷電下較易損毀;改用 PostgreSQL plugin 可顯著降低索引損毀風險。 +4. **PBS 備份涵蓋 CT124**:確認 `/opt` 下各 stack 的資料卷有納入 Proxmox Backup Server 排程,作為索引復原失敗時的最後保險。