# 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), рекомендація.