# NODA1 — Runbook: Memory Stack (діагностика та відновлення)

**Контекст:** Проблеми зі стеком пам'яті на NODA1 трапляються регулярно. Цей runbook — для швидкої діагностики та безпечного перезапуску **без змін конфігурації**.

**Нода:** NODA1 — `node1-daarion` (144.76.224.179)  
**Сервіси:** `dagi-memory-service-node1`, `dagi-qdrant-node1`, `postgres-backup-node1`, `render-pdf-worker-node1`, `dagi-nats-node1`, Caddy

---

## 1) Швидка фіксація стану (read-only, ~1 хв)

```bash
# Загальний стан контейнерів (імена/статуси/порти)
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | egrep 'dagi-memory-service-node1|dagi-qdrant-node1|postgres-backup-node1|render-pdf-worker-node1|dagi-nats-node1| nats '

# Health-статус детально (memory + postgres-backup)
docker inspect dagi-memory-service-node1 --format '{{json .State.Health}}' | jq .
docker inspect postgres-backup-node1     --format '{{json .State.Health}}' | jq . 2>/dev/null || true

# Останні 100/200 рядків логів (таймаути, Qdrant)
docker logs --tail 100 dagi-memory-service-node1
docker logs --tail 200 dagi-qdrant-node1
docker logs --tail 200 postgres-backup-node1
```

---

## 2) Діагностика мережі/доступності memory-service ↔ Qdrant (read-only)

### 2.1 Qdrant "живий" з хоста

```bash
curl -s -o /dev/null -w "Qdrant /healthz HTTP=%{http_code}\n" --connect-timeout 3 http://127.0.0.1:6333/healthz
curl -s -o /dev/null -w "Qdrant /collections HTTP=%{http_code}\n" --connect-timeout 3 http://127.0.0.1:6333/collections
```

### 2.2 Qdrant доступний **з контейнера** memory-service (критично для міжконтейнерної мережі)

```bash
# DNS + TCP + HTTP всередині контейнера
docker exec dagi-memory-service-node1 sh -lc '
  echo "== DNS ==";
  getent hosts dagi-qdrant-node1 || true;
  echo "== TCP 6333 ==";
  (nc -vz -w 2 dagi-qdrant-node1 6333 && echo OK) || echo FAIL;
  echo "== HTTP /healthz ==";
  curl -s -o /dev/null -w "HTTP=%{http_code}\n" --connect-timeout 5 http://dagi-qdrant-node1:6333/healthz || echo FAIL
' 2>/dev/null || echo "docker exec failed"
```

### 2.3 Перевірка "де саме" таймаут: `/collections` і латентність

```bash
# /collections інколи повільний при навантаженні/IO
docker exec dagi-memory-service-node1 sh -lc '
  echo "== /collections latency ==";
  time curl -s -o /dev/null --connect-timeout 5 http://dagi-qdrant-node1:6333/collections || true
' 2>/dev/null
```

> Якщо `healthz` швидкий, а `/collections` "висить" або дає таймаути — проблема частіше **навантаження/IO Qdrant**, а не DNS.

---

## 0) Довести причину: bind vs iptables (read-only)

### 0.1 Bind-порти всередині Qdrant

```bash
docker exec dagi-qdrant-node1 sh -c '
  ss -ltnp | egrep ":(6333|6334)\s" || netstat -ltnp | egrep ":(6333|6334)\s" || cat /proc/net/tcp | head -5
'
```

- **`127.0.0.1:6333`** → причина (A): bind на localhost; фікс: `QDRANT__SERVICE__HOST=0.0.0.0`.
- **`0.0.0.0:6333`** (або в `/proc/net/tcp`: `00000000:18BD`) → причина (B): мережеве блокування; див. 0.3.

### 0.2 Одна мережа для Qdrant і memory

```bash
docker network inspect dagi-network --format '{{json .Containers}}' | jq 'keys'
docker inspect dagi-qdrant-node1 --format '{{json .NetworkSettings.Networks}}' | jq .
docker inspect dagi-memory-service-node1 --format '{{json .NetworkSettings.Networks}}' | jq .
```

### 0.3 iptables DOCKER-USER (часта причина таймаутів між контейнерами)

```bash
iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v
```

**Перевірено 2025-01-28:** У DOCKER-USER є правило `! -s 127.0.0.1/32 -p tcp --dport 6333 -j DROP` (і аналогічно для 8000, 9500, …). Трафік з контейнерів (172.18.x.x) на порт 6333/8000 **DROP**-иться; з хоста (127.0.0.1) — працює. Qdrant при цьому слухає на **0.0.0.0:6333** (`/proc/net/tcp`: `00000000:18BD`). Висновок: причина **(B) iptables**, не bind.

**Фікс (дозволити контейнер→контейнер для 6333 і 8000):** вставити ACCEPT **перед** DROP (наприклад у `rc.local` або окремому скрипті, щоб правило пережило перезавантаження):

```bash
# Дозволити трафік з мережі контейнерів (dagi-network 172.18.0.0/16) на Qdrant і memory-service
iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp -m tcp --dport 6333 -j ACCEPT
iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp -m tcp --dport 8000 -j ACCEPT
```

Після цього: `docker restart dagi-memory-service-node1`, дочекатися 30–60 с, перевірити health.

---

## 0.4 Збереження правил DOCKER-USER після reboot

### A) iptables-persistent (рекомендовано, Ubuntu/Debian)

**A0) Зафіксувати поточні правила:**
```bash
iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v --line-numbers
```

**A1) Встановити пакети:**
```bash
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent netfilter-persistent
```

**A2) Зберегти правила:**
```bash
iptables-save > /etc/iptables/rules.v4
# (IPv6 за потреби) ip6tables-save > /etc/iptables/rules.v6
```

**A3) Увімкнути відновлення на старті:**
```bash
systemctl enable netfilter-persistent
systemctl restart netfilter-persistent
systemctl status netfilter-persistent --no-pager -l
```

**A3.1) Готовність без reboot (read-only)** — переконатися, що netfilter-persistent підхопив `rules.v4` і що Docker не перетре ланцюги:

```bash
# 1) netfilter-persistent читає rules.v4
sudo netfilter-persistent status
sudo systemctl status netfilter-persistent --no-pager -l

# 2) Завантажені правила і звірка з файлом
sudo iptables-save | grep -n "^\*filter"
sudo iptables-save | egrep "DOCKER-USER|172\.18\.0\.0/16|dport (6333|8000)" -n

# 3) У файлі правила є
sudo grep -n "DOCKER-USER" /etc/iptables/rules.v4
sudo egrep -n "172\.18\.0\.0/16.*dport (6333|8000).*ACCEPT" /etc/iptables/rules.v4 || echo "MISSING_IN_FILE"

# 4) ACCEPT стоять ПЕРЕД DROP
sudo iptables -L DOCKER-USER -n --line-numbers | head -n 25
```

Очікувано: `iptables -L DOCKER-USER --line-numbers` показує ACCEPT для 172.18.0.0/16 на 6333 і 8000 **вище** за DROP.

**A4) Після reboot — валідація:**
```bash
iptables -S DOCKER-USER | egrep '172\.18\.0\.0/16.*dport (6333|8000).*ACCEPT' || echo "MISSING"
docker ps --format 'table {{.Names}}\t{{.Status}}' | egrep 'dagi-qdrant-node1|dagi-memory-service-node1'
curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" http://127.0.0.1:8000/health
```

**A4.1) Після reboot — ~30 секунд перевірок:**
```bash
sudo iptables -S DOCKER-USER | egrep '172\.18\.0\.0/16.*dport (6333|8000).*ACCEPT' || echo "MISSING"

docker ps --format 'table {{.Names}}\t{{.Status}}' | egrep 'dagi-qdrant-node1|dagi-memory-service-node1'

curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" http://127.0.0.1:8000/health
```

### B) Альтернатива без пакетів: systemd unit на boot

**B1) Створити unit** `/etc/systemd/system/dagi-iptables-allow-dagi-network.service`:
```ini
[Unit]
Description=Allow dagi-network container traffic to Qdrant and Memory ports in DOCKER-USER
After=network-online.target docker.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/bin/sh -c '\
  iptables -C DOCKER-USER -s 172.18.0.0/16 -p tcp --dport 6333 -j ACCEPT 2>/dev/null || \
  iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp --dport 6333 -j ACCEPT; \
  iptables -C DOCKER-USER -s 172.18.0.0/16 -p tcp --dport 8000 -j ACCEPT 2>/dev/null || \
  iptables -I DOCKER-USER 1 -s 172.18.0.0/16 -p tcp --dport 8000 -j ACCEPT; \
  iptables -S DOCKER-USER'
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
```

**B2) Увімкнути і запустити:**
```bash
systemctl daemon-reload
systemctl enable --now dagi-iptables-allow-dagi-network.service
systemctl status dagi-iptables-allow-dagi-network.service --no-pager -l
```

**Страхувальний варіант (без конфлікту з A):** залишити A як є і **додати B-unit лише як fallback**. Unit ідемпотентний (`iptables -C` перед вставкою). Корисно, якщо на хості є UFW/інші інструменти, що переупорядковують правила після boot.

### Якщо після reboot правила зникнуть

**1) iptables-nft vs iptables-legacy або netfilter-persistent тягне не той бекенд:**
```bash
iptables -V
update-alternatives --display iptables 2>/dev/null || true
sudo journalctl -u netfilter-persistent -b --no-pager | tail -200
```

**2) Docker/ufw змінюють форвардинг або ланцюги після старту:**
```bash
sudo journalctl -u docker -b --no-pager | tail -200
sudo iptables -S DOCKER-USER
```

Якщо змішування legacy/nft — стандартизувати backend (окремий крок).

---

## 0.5 Перший крок при таймаутах між контейнерами

При будь-яких таймаутах memory↔Qdrant або інших контейнер↔контейнер **спочатку** перевірити DOCKER-USER:

```bash
iptables -S DOCKER-USER
iptables -L DOCKER-USER -n -v --line-numbers
```

Якщо є `! -s 127.0.0.1/32 ... --dport 6333 -j DROP` без попереднього ACCEPT для 172.18.0.0/16 — застосувати фікс з 0.3 і зберегти правила (0.4).

---

## 3) Діагностика самого memory-service (read-only)

```bash
# Health endpoint з хоста
curl -s -i --max-time 5 http://127.0.0.1:8000/health | head

# Ресурси контейнера (CPU throttling / RAM pressure)
docker stats --no-stream dagi-memory-service-node1 dagi-qdrant-node1 2>/dev/null

# Env (тільки читання) — чи не з'їхав QDRANT_HOST/PORT
docker exec dagi-memory-service-node1 sh -lc 'env | egrep "QDRANT|MEM|VECTOR|COLLECTION|TIMEOUT" | sort' 2>/dev/null || true
```

---

## 4) postgres-backup-node1 unhealthy — мінімальна діагностика (read-only)

1. Що саме перевіряє healthcheck і останній output:

```bash
docker inspect postgres-backup-node1 --format '{{json .Config.Healthcheck}}' | jq .
docker inspect postgres-backup-node1 --format '{{range .State.Health.Log}}{{println .End " exit=" .ExitCode " " .Output}}{{end}}'
```

2. Логи контейнера:

```bash
docker logs --tail 200 postgres-backup-node1
```

> Типові причини "Up, але unhealthy": healthcheck звертається до Postgres по hostname, який не резолвиться/не доступний; немає прав/пароля; або backup-процес завис і healthcheck очікує lock.

**Перевірено 2025-01-28:** Логи показують **pg_dump version mismatch**: Postgres сервер 16.11, pg_dump у контейнері 15.14. Healthcheck повертає 503, бо внутрішній HTTP-сервіс бекапу відповідає 503 при невдалому бекапі.

**Наступний фікс (зміна образу):** оновити образ postgres-backup так, щоб **pg_dump був 16.x** (базуватись на `postgres:16` або встановити `postgresql-client-16` у backup image). Після оновлення образу — перезапуск контейнера backup.

---

## 4.1 render-pdf-worker: NATS timeout

- Перевірити env: `docker inspect render-pdf-worker-node1 --format '{{json .Config.Env}}' | jq -r '.[]' | egrep -i NATS'`.
- **Перевірено 2025-01-28:** `NATS_URL=nats://nats:4222` — hostname **nats**, тоді як контейнер у мережі називається **dagi-nats-node1**. Якщо аліасу `nats` немає, воркер не з’єднається з NATS. **Наступний фікс (один із варіантів):** (1) додати **network alias** `nats` для контейнера `dagi-nats-node1` у docker-compose; (2) змінити `NATS_URL` на `nats://dagi-nats-node1:4222` у воркері; (3) аліас через compose `networks: … aliases: [nats]`.

---

## 4.2 Caddy failed: конфлікт порту

- **Перевірено 2025-01-28:** `journalctl -u caddy`: **"listen tcp :443: bind: address already in use"**. Порти 80/443 зайняті **nginx**. Caddy не стартує, поки nginx тримає ці порти.

**Наступний фікс (архітектура):** це не поломка, а конфлікт. Варіанти: (1) залишити **nginx** єдиним reverse proxy і вимкнути Caddy; (2) перейти на **Caddy** (вимкнути nginx); (3) Caddy слухає інші порти і nginx проксує до нього (рідко має сенс).

---

## 5) Точковий перезапуск БЕЗ зміни конфігурації

### 5.1 Перезапуск лише memory-service

```bash
docker restart dagi-memory-service-node1

# Дати прогрітись і перевірити health
sleep 30
docker inspect dagi-memory-service-node1 --format '{{.State.Health.Status}}'
curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" --max-time 5 http://127.0.0.1:8000/health
docker logs --tail 80 dagi-memory-service-node1
```

### 5.2 Якщо знову таймаут до Qdrant — перезапуск Qdrant → потім memory-service

```bash
docker restart dagi-qdrant-node1
sleep 20
docker restart dagi-memory-service-node1

sleep 40
docker inspect dagi-memory-service-node1 --format '{{.State.Health.Status}}'
```

### 5.3 render-pdf-worker-node1 (Exited 1) — запустити і глянути причину падіння

```bash
docker start render-pdf-worker-node1
sleep 2
docker ps -a --filter name=render-pdf-worker-node1 --format 'table {{.Names}}\t{{.Status}}'
docker logs --tail 200 render-pdf-worker-node1
```

### 5.4 postgres-backup (unhealthy) — лише restart за потреби

```bash
docker restart postgres-backup-node1
docker logs --tail 200 postgres-backup-node1
```

---

## 6) Caddy failed (read-only + точковий restart за потреби)

```bash
systemctl status caddy --no-pager -l
journalctl -u caddy -n 200 --no-pager
```

Якщо треба просто підняти сервіс без змін конфігурації:

```bash
systemctl restart caddy
systemctl status caddy --no-pager -l
```

---

## 7) Що саме зібрати після "4-го падіння" (щоб знайти повторювану причину)

1. **Час** останнього падіння memory-service (з `docker logs --since ...`).
2. `docker inspect ...Health.Log` для memory-service та postgres-backup.
3. Латентність **Qdrant /collections** та чи проходить **nc** з контейнера memory-service на `dagi-qdrant-node1:6333`.
4. `docker stats --no-stream` для memory + qdrant у момент проблеми.

Якщо надаси ці 4 блоки в одному повідомленні — можна точніше локалізувати: **мережа між контейнерами**, **Qdrant під навантаженням/IO**, або **healthcheck memory-service надто "агресивний"** відносно реального часу відповіді Qdrant.

---

## 8) Однією командою (копіпаста на NODA1)

**Швидка діагностика (read-only):**

```bash
echo "=== Qdrant from host ===" && curl -s -o /dev/null -w "%{http_code}\n" --connect-timeout 3 http://127.0.0.1:6333/healthz
echo "=== Qdrant from memory container ===" && docker exec dagi-memory-service-node1 sh -c 'curl -s -o /dev/null -w "%{http_code}\n" --connect-timeout 5 http://dagi-qdrant-node1:6333/healthz' 2>/dev/null || echo "fail"
echo "=== Memory /health ===" && curl -s -o /dev/null -w "%{http_code}\n" --max-time 5 http://127.0.0.1:8000/health
docker ps -a --format 'table {{.Names}}\t{{.Status}}' | egrep 'dagi-memory-service-node1|dagi-qdrant-node1|postgres-backup-node1|render-pdf-worker-node1'
```

**Тільки перезапуск memory + render-pdf-worker (без Qdrant):**

```bash
docker restart dagi-memory-service-node1
docker start render-pdf-worker-node1
echo "Waiting 45s for memory health..."
sleep 45
docker ps --filter name=dagi-memory-service-node1 --filter name=render-pdf-worker-node1 --format "table {{.Names}}\t{{.Status}}"
```

---

## Рекомендація для вашого кейсу (щоб не ловити це 5-й раз)

1. **Закріпити правила DOCKER-USER** одним із способів **0.4 A або B** (A краще — iptables-persistent).
2. **Додати в runbook** перевірку DOCKER-USER як перший крок при будь-яких таймаутах між контейнерами (див. 0.5).
3. **Окремо запланувати 3 техборги:** (1) backup image з pg16; (2) NATS alias/URL для render-pdf-worker; (3) reverse-proxy single-owner (nginx або Caddy).

---

## Статус по інших знайдених проблемах (куди рухатись далі)

- **postgres-backup:** pg_dump version mismatch (сервер 16.x, клієнт 15.x) → потрібен образ з pg16.
- **render-pdf-worker:** NATS hostname `nats` vs реальний контейнер `dagi-nats-node1` → alias або зміна NATS_URL.
- **Caddy:** конфлікт портів з nginx (80/443) → один reverse proxy (nginx-only або міграція на Caddy).

| Проблема | Що потрібно |
|----------|-------------|
| **postgres-backup** | Образ з `pg_dump 16.x` (зміна образу неминуча). |
| **render-pdf-worker** | Або network alias `nats` для `dagi-nats-node1`, або зміна `NATS_URL` на `nats://dagi-nats-node1:4222` (мінімальна правка compose). |
| **Caddy** | Конфлікт портів з nginx — вибір одного reverse proxy. |

**Готові патчі:** див. **node2/docs/NODA1-TECHBORGS-PATCHES.md** — зібрані YAML-блоки з NODA1, точні diff, порядок застосування та acceptance checks для патчів #1 (postgres-backup pg16), #2 (render-pdf-worker NATS alias), #3 (Caddy vs nginx).

---

**Скрипт діагностики (на NODA1):** `node2/scripts/noda1-memory-diagnose.sh`  
Запуск на сервері: скопіювати скрипт на NODA1 і виконати `bash noda1-memory-diagnose.sh`.  
Або з локальної машини: `ssh root@144.76.224.179 'bash -s' < node2/scripts/noda1-memory-diagnose.sh`

**Останнє оновлення:** 2025-01-28  
**Історія:** 4-й випадок; додано постійне збереження DOCKER-USER (A/B), перший крок при таймаутах (0.5), наступні фікси (backup pg16, NATS alias, Caddy vs nginx), рекомендація.
