# NODA1 — Точні патчі для трьох техборгів

**Source of truth:** repo (node2) — `docs/NODA1-TECHBORGS-PATCHES.md`  
**Copy on NODA1:** `/opt/microdao-daarion/docs/NODA1-TECHBORGS-PATCHES.md`  
**Code commit:** d0188fd  
**Docs commit:** fb6b92b  
**Last sync:** 2026-01-28

---

Документ містить **зібрані з NODA1** YAML-фрагменти (read-only) і **мінімальні, безпечні патчі** для postgres-backup (pg16), render-pdf-worker (NATS hostname), Caddy vs nginx.

**Compose-файли на NODA1:**
- Основний стек: `/opt/microdao-daarion/docker-compose.node1.yml`
- Бекапи: `/opt/microdao-daarion/docker-compose.backups.yml`
- Effective config: `cd /opt/microdao-daarion && docker compose -f docker-compose.node1.yml config` → 766 рядків

---

## 0) Safety + rollback (~2 хв)

```bash
# 0.1 Backup compose файлів перед правкою
cd /opt/microdao-daarion
cp -a docker-compose.node1.yml docker-compose.node1.yml.bak.$(date +%F_%H%M%S)
cp -a docker-compose.backups.yml docker-compose.backups.yml.bak.$(date +%F_%H%M%S)

# 0.2 Зняти поточні images (для швидкого повернення)
docker images --format 'table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}' | head -50

# 0.3 Rollback (якщо треба)
# cp docker-compose.node1.yml.bak.<ts> docker-compose.node1.yml
# cp docker-compose.backups.yml.bak.<ts> docker-compose.backups.yml
# docker compose -f docker-compose.node1.yml up -d
# docker compose -f docker-compose.backups.yml up -d
```

**Перед застосуванням — валідація YAML (ловить синтаксичні помилки до будь-якого `up -d`):**

```bash
docker compose -f /opt/microdao-daarion/docker-compose.node1.yml config >/dev/null && echo "node1 compose OK"
docker compose -f /opt/microdao-daarion/docker-compose.backups.yml config >/dev/null && echo "backups compose OK"
```

**Примітка про service name vs container_name (пастка №1):** `docker compose up -d <service>` працює тільки з **іменем сервісу** з compose, а `docker logs` / `docker exec` — по **імені контейнера** (container_name). Щоб не плутатись:

```bash
# Показати відповідність service → container
cd /opt/microdao-daarion
docker compose -f docker-compose.node1.yml ps
docker compose -f docker-compose.backups.yml ps
```

---

## 1) Зібрані YAML-блоки (read-only)

### 1.1 qdrant (з effective config)

```yaml
  qdrant:
    container_name: dagi-qdrant-node1
    healthcheck:
      test: ["CMD", "true"]
      timeout: 10s
      interval: 30s
      retries: 3
    image: qdrant/qdrant:v1.7.4
    networks:
      dagi-network: null
    ports:
      - "6333:6333"
      - "6334:6334"
    restart: unless-stopped
    ulimits:
      nofile: { soft: 65536, hard: 65536 }
    volumes:
      - qdrant-data-node1:/qdrant/storage
```

### 1.2 nats (з effective config)

```yaml
  nats:
    command: ["-js"]
    container_name: dagi-nats-node1
    image: nats:2.10-alpine
    networks:
      dagi-network: null
    ports:
      - "4222:4222"
    restart: unless-stopped
    volumes:
      - nats-data-node1:/data
```

### 1.3 render-pdf-worker (з effective config)

```yaml
  render-pdf-worker:
    build:
      context: /opt/microdao-daarion/services/render-pdf-worker
      dockerfile: Dockerfile
    container_name: render-pdf-worker-node1
    depends_on:
      artifact-registry: { condition: service_started, required: true }
      minio: { condition: service_started, required: true }
      nats: { condition: service_started, required: true }
    environment:
      ARTIFACT_REGISTRY_URL: http://artifact-registry:9220
      MINIO_ACCESS_KEY: minioadmin
      MINIO_BUCKET: artifacts
      MINIO_ENDPOINT: minio:9000
      MINIO_SECRET_KEY: minioadmin
      MINIO_SECURE: "false"
      NATS_URL: nats://nats:4222
    networks:
      dagi-network: null
    restart: unless-stopped
```

### 1.4 postgres-backup (з docker-compose.backups.yml)

```yaml
services:
  postgres-backup:
    image: prodrigestivill/postgres-backup-local:15
    container_name: postgres-backup-node1
    restart: unless-stopped
    environment:
      POSTGRES_HOST: dagi-postgres
      POSTGRES_DB: daarion_main,daarion_memory,rag
      POSTGRES_USER: daarion
      POSTGRES_PASSWORD: DaarionDB2026!
      SCHEDULE: "@every 6h"
      BACKUP_KEEP_DAYS: 7
      BACKUP_KEEP_WEEKS: 4
      BACKUP_KEEP_MONTHS: 6
      POSTGRES_EXTRA_OPTS: "-Z9 --schema=public --blobs"
    volumes:
      - /opt/backups/postgres:/backups
    networks:
      - dagi-network

networks:
  dagi-network:
    external: true
```

### 1.5 Проксі (системні сервіси, не compose)

- **nginx:** active (running), тримає 80/443, конфіг: `/etc/nginx/nginx.conf` + `sites-enabled/*`, `conf.d/*`.
- **Caddy:** failed — "listen tcp :443: bind: address already in use" (nginx вже слухає 443). Версія Caddy: v2.10.2.

---

## 2) Патч #1: postgres-backup (pg_dump 16.x)

**Ціль:** pg_dump версії 16 відповідає Postgres 16.11 на сервері.

**Файл:** `/opt/microdao-daarion/docker-compose.backups.yml`

**Зміна (1 рядок):**

```diff
-    image: prodrigestivill/postgres-backup-local:15
+    image: prodrigestivill/postgres-backup-local:16
```

Якщо тегу `:16` немає на Docker Hub для цього образу — варіанти:
- Використати інший образ на базі `postgres:16` (наприклад, з postgresql-client-16).
- Або зібрати власний Dockerfile на базі `postgres:16` і викликати `pg_dump` з нього.

**Після правки:**

```bash
cd /opt/microdao-daarion
docker compose -f docker-compose.backups.yml pull postgres-backup
docker compose -f docker-compose.backups.yml up -d postgres-backup
docker logs --tail 50 postgres-backup-node1
```

---

## 3) Патч #2: render-pdf-worker (NATS hostname)

**Проблема:** `NATS_URL=nats://nats:4222`, а сервіс у мережі називається `dagi-nats-node1` (container_name), тому DNS `nats` не резолвиться.

**Рекомендація: варіант A (менш інвазивний)** — додати alias `nats` для сервісу `nats` у `dagi-network`. Інші клієнти (якщо теж використовують `nats:4222`) продовжать працювати без змін.

**Нюанс (alias тільки на рівні сервісу):** зміна має бути саме у **сервісі** (не в секції top-level `networks:`). У docker-compose.node1.yml сервіс називається `nats` (container_name: dagi-nats-node1). Якщо у вашому файлі сервіс має інше ім'я (наприклад `dagi-nats-node1`), використовуйте його як ключ:

```yaml
services:
  nats:
    container_name: dagi-nats-node1
    networks:
      dagi-network:
        aliases:
          - nats
```

Або якщо сервіс названо `dagi-nats-node1`:

```yaml
services:
  dagi-nats-node1:
    networks:
      dagi-network:
        aliases:
          - nats
```

Якщо зараз `networks: [dagi-network]` або `dagi-network: null` — замінити на мапу з `aliases` як вище.

**Файл:** `/opt/microdao-daarion/docker-compose.node1.yml`

**Знайти блок `nats:`** (приблизно такий):

```yaml
  nats:
    command:
      - -js
    container_name: dagi-nats-node1
    image: nats:2.10-alpine
    networks:
      dagi-network: null
```

**Замінити на (додати `aliases`):**

```yaml
  nats:
    command:
      - -js
    container_name: dagi-nats-node1
    image: nats:2.10-alpine
    networks:
      dagi-network:
        aliases:
          - nats
    ports:
      - "4222:4222"
    restart: unless-stopped
    volumes:
      - nats-data-node1:/data
```

Тобто замість `dagi-network: null` використати:

```yaml
    networks:
      dagi-network:
        aliases:
          - nats
```

**Альтернатива B:** у сервісі `render-pdf-worker` змінити env:
```yaml
      NATS_URL: nats://dagi-nats-node1:4222
```

**Після правки (варіант A):**

```bash
cd /opt/microdao-daarion
docker compose -f docker-compose.node1.yml up -d nats
docker compose -f docker-compose.node1.yml up -d render-pdf-worker
docker logs --tail 80 render-pdf-worker-node1
```

---

## 4) Render-PDF-Worker: діагностика та idle timeout

**Висновок з перевірок A/B:** Підключення воркера до NATS працює (alias `nats` резолвиться у `dagi-network`). Помилка `nats.errors.TimeoutError` у логах — це **таймаут очікування повідомлень** (`sub.next_msg()`), а не з’єднання. Subject підписки: `artifact.job.render_pdf.requested`. Якщо задач немає, воркер отримує timeout і без обробки винятку завершується (exit 1).

**A) Довести підключення до NATS (read-only):**
- TCP з воркера (коли контейнер Up): `docker exec render-pdf-worker-node1 sh -lc 'nc -vz -w 2 nats 4222 && echo NATS_TCP_OK'` (у контейнері memory-service немає `nc`, тому перевірку можна робити з іншого контейнера з `nc` або через `getent hosts nats`).
- Логи NATS: `docker logs --tail 200 dagi-nats-node1 | egrep -i 'client|connect|disconnect'`.

**B) Env і subject:** У воркера лише `NATS_URL=nats://nats:4222`; subject/queue задані в коді: `artifact.job.render_pdf.requested`.

**C) Варіант без зміни коду:** У compose вже є `restart: unless-stopped`; додано `stop_grace_period: 30s`. Воркер буде автоматично перезапускатися після exit при idle.

**D) Варіант з мінімальною зміною коду (рекомендовано):** У `main.py` обробити `nats.errors.TimeoutError` у циклі очікування повідомлень — не завершувати процес, а робити `continue`:

```python
import nats.errors
# ...
while True:
    try:
        msg = await sub.next_msg()
    except nats.errors.TimeoutError:
        continue
    # обробка msg
```

Після патчу D воркер залишається активним при відсутності задач без рестарт-циклів.

**Спостережуваність NATS (без підвищеного логування):** стандартні логи NATS не показують connect/disconnect клієнтів. Корисно мати health/metrics endpoint або періодичну перевірку `connz` (якщо дозволено конфігом NATS monitoring port), щоб швидко довести, що клієнт реально підключився. Не обов’язково зараз, але зменшує час діагностики.

---

## 5) Патч #3: Caddy vs nginx (конфлікт 80/443)

**Поточний стан:** nginx active, тримає 80/443; Caddy failed через "address already in use".

**Рекомендація (найпростіший варіант):** **nginx єдиний reverse proxy** — вимкнути Caddy, щоб уникнути конфлікту і подвійної конфігурації.

**Мінімальний патч (без зміни nginx):**

```bash
sudo systemctl stop caddy
sudo systemctl disable caddy
```

Якщо потрібно саме Caddy як єдиний проксі — тоді вимкнути nginx, перенести конфіг з nginx у Caddyfile і налаштувати Caddy на 80/443 (окремий техборг).

---

## Порядок застосування патчів (рекомендований)

### 1.1 Патч #2 (NATS alias) — першим, бо розблоковує render-pdf-worker

Використовуйте **ім'я сервісу** з compose (`nats`, `render-pdf-worker`), не container_name (`dagi-nats-node1`, `render-pdf-worker-node1`) для `docker compose up -d`.

```bash
cd /opt/microdao-daarion

# Після внесення alias у docker-compose.node1.yml
docker compose -f docker-compose.node1.yml pull nats 2>/dev/null || true
docker compose -f docker-compose.node1.yml up -d nats

# Перевірка alias резолвиться з воркера
docker start render-pdf-worker-node1 2>/dev/null || true
docker exec render-pdf-worker-node1 sh -lc 'getent hosts nats && nc -vz -w 2 nats 4222' 2>/dev/null || true

# Перепідняти воркер (service name)
docker compose -f docker-compose.node1.yml up -d render-pdf-worker 2>/dev/null || true
docker logs --tail 120 render-pdf-worker-node1
```

Якщо `render-pdf-worker-node1` не керується compose (standalone container):

```bash
docker restart render-pdf-worker-node1
docker logs --tail 120 render-pdf-worker-node1
```

**Якщо alias додано, але worker все одно не бачить nats (пастка №2):** найчастіше воркер не в тій же мережі, або він standalone і не приєднаний до `dagi-network`.

```bash
# 1) Переконатися, що nats і worker в одній мережі
docker inspect dagi-nats-node1 --format '{{json .NetworkSettings.Networks}}' | jq .
docker inspect render-pdf-worker-node1 --format '{{json .NetworkSettings.Networks}}' | jq .

# 2) Якщо worker standalone і не в dagi-network — тимчасово приєднати (операційний workaround)
# УВАГА: це змінює стан хоста (але не compose). Застосовувати лише як швидкий фікс.
# docker network connect dagi-network render-pdf-worker-node1

# 3) Повторна перевірка
docker exec render-pdf-worker-node1 sh -lc 'getent hosts nats && nc -vz -w 2 nats 4222' 2>/dev/null || true
```

### 1.2 Патч #1 (postgres-backup образ pg16)

```bash
cd /opt/microdao-daarion

# Після заміни tag :15 -> :16 у docker-compose.backups.yml
docker compose -f docker-compose.backups.yml pull postgres-backup 2>/dev/null || true
docker compose -f docker-compose.backups.yml up -d postgres-backup

docker ps --filter name=postgres-backup-node1 --format 'table {{.Names}}\t{{.Status}}'
docker logs --tail 200 postgres-backup-node1
docker inspect postgres-backup-node1 --format '{{.State.Health.Status}}'
```

### 1.3 Патч #3 (nginx-only proxy) — вимкнути Caddy

```bash
systemctl stop caddy || true
systemctl disable caddy || true
systemctl status caddy --no-pager -l || true

# Переконатися що nginx тримає 80/443
systemctl status nginx --no-pager -l
ss -ltnp | egrep ':(80|443)\s'
```

---

## Sync from repo → NODA1

```bash
# локально (node2)
cd /Users/apple/node2

# (опційно) показати, що саме деплоїмо
git rev-parse --short HEAD
git show -s --format=%ci

# 1) Код воркера
scp -p services/render-pdf-worker/app/main.py \
  root@144.76.224.179:/opt/microdao-daarion/services/render-pdf-worker/app/main.py

# 2) Документація
scp -p docs/NODA1-TECHBORGS-PATCHES.md \
  root@144.76.224.179:/opt/microdao-daarion/docs/NODA1-TECHBORGS-PATCHES.md

scp -p docs/NODA1-MEMORY-RUNBOOK.md \
  root@144.76.224.179:/opt/microdao-daarion/docs/NODA1-MEMORY-RUNBOOK.md

# 3) На NODA1: rebuild + restart + лог
ssh root@144.76.224.179 '
  cd /opt/microdao-daarion &&
  docker compose -f docker-compose.node1.yml ps &&
  docker compose -f docker-compose.node1.yml build render-pdf-worker &&
  docker compose -f docker-compose.node1.yml up -d render-pdf-worker &&
  docker logs --tail 120 render-pdf-worker-node1
'
```

Якщо у compose сервіс називається інакше (наприклад `render-pdf-worker-node1` лише як container_name), замінити **лише** аргумент у `build` / `up -d`. Командою `docker compose ... ps` це одразу видно.

**Швидка валідація після синку (на NODA1):**

```bash
docker ps --filter name=render-pdf-worker --format 'table {{.Names}}\t{{.Status}}'
docker logs --tail 120 render-pdf-worker-node1 | egrep -i 'idle, no messages|subscribed|error|exception' || true
```

---

## Acceptance checks (ловлять регрес одразу)

```bash
# Memory/Qdrant (регрес на DOCKER-USER або мережу)
curl -s -o /dev/null -w "memory /health HTTP=%{http_code}\n" http://127.0.0.1:8000/health
docker exec dagi-memory-service-node1 sh -lc 'curl -s -o /dev/null -w "qdrant-from-memory HTTP=%{http_code}\n" --connect-timeout 5 http://dagi-qdrant-node1:6333/healthz' 2>/dev/null || true
iptables -L DOCKER-USER -n --line-numbers | head -25

# NATS: відрізнити "немає підключення" від "немає задач" (якщо воркер Exited — перевірка з іншого контейнера)
# Якщо в контейнері немає nc — альтернативи: getent hosts nats; curl -v --connect-timeout 2 telnet://nats:4222; python -c "import socket; s=socket.socket(); s.settimeout(2); s.connect(('nats',4222)); s.close(); print('TCP OK')"
docker exec render-pdf-worker-node1 sh -lc 'getent hosts nats && nc -vz -w 2 nats 4222' 2>/dev/null || echo "worker cannot reach nats"

# render-pdf-worker: якір — підтвердження що піднялась версія з idle-heartbeat
docker logs --tail 200 render-pdf-worker-node1 | egrep -i 'idle, no messages' && echo "worker heartbeat OK" || echo "heartbeat not seen (yet)"

# postgres-backup: mismatch зник (конкретний сигнал); якщо 10–15 хв лишається "starting" — зібрати Health.Log і логи
docker inspect postgres-backup-node1 --format '{{json .State.Health}}' | jq .
docker inspect postgres-backup-node1 --format '{{range .State.Health.Log}}{{println .End " exit=" .ExitCode " " .Output}}{{end}}'
docker logs --tail 200 postgres-backup-node1
docker logs --tail 200 postgres-backup-node1 | egrep -i 'pg_dump.*version|mismatch' && echo "STILL MISMATCH" || echo "mismatch not seen in tail"
docker inspect postgres-backup-node1 --format '{{.State.Health.Status}}'

# Proxy ownership: nginx ок, caddy off
systemctl is-active nginx
systemctl is-enabled caddy 2>/dev/null || true
ss -ltnp | egrep ':(80|443)\s'
```

---

## 6) Контрольний список приймання після патчів

- [ ] `docker compose -f docker-compose.node1.yml config` без помилок.
- [ ] `docker compose -f docker-compose.backups.yml config` без помилок.
- [ ] `docker compose up -d` (відповідно до ваших файлів) без помилок.
- [ ] **postgres-backup:** health 200/healthy або лог без "version mismatch"; успішний pg_dump.
- [ ] **render-pdf-worker:** не падає з NATS timeout; або стабільно працює як long-running worker.
- [ ] **Проксі:** або nginx, або Caddy стабільно тримає 80/443; інший вимкнений (disable).

---

**Останнє оновлення:** 2026-01-28  
**Джерело:** зібрані фрагменти з NODA1 (read-only), effective config з `docker-compose.node1.yml` та `docker-compose.backups.yml`.
