# 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. docker-compose.node1.yml # cp docker-compose.backups.yml.bak. 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 ` працює тільки з **іменем сервісу** з 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`.