refactor(ai): OCR sidecar canonical naming cleanup — typhoon→np-dms, remove hardcoded keys, asyncio.to_thread, ADR-040/041
CI / CD Pipeline / build (push) Successful in 7m37s
CI / CD Pipeline / deploy (push) Failing after 20m15s

This commit is contained in:
2026-06-20 16:37:04 +07:00
parent d418d791a4
commit a80ebef285
70 changed files with 5762 additions and 452 deletions
@@ -0,0 +1,210 @@
<!-- File: specs/06-Decision-Records/ADR-040-ocr-sidecar-refactor.md -->
<!-- Change Log
- 2026-06-20: Created initial ADR-040 documenting OCR sidecar refactor decisions.
- Supersedes ADR-033 §7 (X-API-Key sidecar auth) in favor of network isolation.
- Preserves resolved GPU policies (Adaptive Residency, CPU Fallback, LLM-First Ownership).
- Aligns with ADR-036 Profile-Only Parameter Governance.
- References ADR-041 for server consolidation enabling network-only auth.
-->
# ADR-040: OCR Sidecar Refactor — Pure Compute Worker, Preserved GPU Policy, Network-Trust Boundary
**Status:** Proposed
**Date:** 2026-06-20
**Supersedes:** ADR-033 §7 (X-API-Key sidecar auth)
**Amends:** ADR-036 §5 (sidecar contract), ADR-034 (model identity unchanged)
**Related Documents:**
- [ADR-016: Security & Authentication](./ADR-016-security-authentication.md)
- [ADR-008: Email Notification Strategy](./ADR-008-email-notification-strategy.md)
- [ADR-029: Dynamic Prompt Management](./ADR-029-dynamic-prompt-management.md)
- [ADR-037: Active Prompt System](./ADR-037-active-prompt-system.md)
- [ADR-035: AI Pipeline & OCR Integration](./ADR-035-ai-pipeline-ocr-integration.md)
- [ADR-041: Server Consolidation](./ADR-041-server-consolidation.md)
- [CONTEXT.md](../../00-overview/CONTEXT.md)
- [OCR Sidecar Refactor Plan - Claude](../../../docs/ocr-sidecar-refactor-plan-cluade.md)
- [OCR Sidecar Refactor Plan - Qwen](../../../docs/ocr-sidecar-refactor-plan-qwen.md)
> **Note:** ADR numbers 038039 are intentionally reserved/skipped.
---
## 🎯 Context and Problem Statement
### Current Architecture
OCR Sidecar บน Desk-5439 (RTX 5060 Ti 16GB) ทำหน้าที่เป็น FastAPI HTTP service สำหรับ:
- `/ocr` - สกัดข้อความจาก PDF ผ่าน Typhoon OCR (np-dms-ocr via Ollama)
- `/embed` - สร้าง vector embedding ผ่าน BGE-M3
- `/rerank` - จัดลำดับผลลัพธ์ retrieval ผ่าน FlagReranker
- `/normalize` - normalize ภาษาไทย (ใช้โดย ThaiPreprocessProcessor)
### Problems Identified
จากการทบทวนสองแผน refactor (Claude + Qwen) พบปัญหาดังนี้:
1. **Security Bug:** Hardcoded default API key (`lcbp3-dms-ocr-sidecar-secure-token-2026`) ใน `app.py` — หาก leak จะไม่สามารถ rotate ได้โดยไม่ rebuild container
2. **Synchronous Blocking I/O:** `process_ocr` ใช้ `httpx.Client` แบบ sync ทำให้ block event loop ของ FastAPI
3. **Deprecated Startup Pattern:** ใช้ `@app.on_event("startup")` แทน `lifespan` context manager
4. **Hardcoded keep_alive:** `process_ocr` บังคับ `keep_alive: 0` แต่ไม่ได้เรียก `calculate_ocr_residency()` จาก `residency_policy.py` — ทำให้ Adaptive OCR Residency policy ไม่ทำงาน
5. **Hardcoded Runtime Parameters:** `temperature`, `top_p`, `repeat_penalty`, `max_tokens` ถูก hardcode ใน sidecar แทนการดึงจาก `ai_execution_profiles` (ADR-036 Profile-Only Parameter Governance)
6. **Path Traversal Vulnerability:** `/ocr` endpoint เปิดไฟล์ตาม `req.pdfPath` โดยไม่มี canonicalization/whitelist — เสี่ยง arbitrary file read (ADR-016)
7. **Cross-Host Trust Gap:** ปัจจุบัน sidecar อยู่บน Desk-5439 (192.168.10.100) และ backend อยู่บน QNAP (192.168.10.8) — "Docker internal network" เป็นเท็จ ต้องพึ่ง VLAN/firewall ACL
8. **Mutable Default Argument:** `process_with_typhoon_ocr(pdf_path, ..., options_override={})` — Python anti-pattern
### Conflict with Canonical Specs
การทบทวนทั้งสองแผนพบว่า:
- **Claude** สมมติ `np-dms-ai = llama3.2 3B (~23GB)` แต่ ADR-034/CONTEXT ระบุ `np-dms-ai` runtime คือ Typhoon-2.5 (~78B) — VRAM budget ผิด
- **ทั้งสองแผน** เสนอลบ `vram_monitor.py` / `residency_policy.py` และบังคับ BGE+Reranker GPU-resident — ละเมิด LLM-First GPU Ownership + CPU Fallback Retrieval ที่ CONTEXT.md ได้ resolve ไว้แล้ว
- **ทั้งสองแผน** ถือ `keep_alive` เป็น fixed config value — ละเมิด ADR-036 Gap-2 (keep_alive = lazy resource param via residency policy)
---
## ⚙️ Decision Drivers
* **Preserve Resolved GPU Policy:** Adaptive OCR Residency + CPU Fallback Retrieval + LLM-First GPU Ownership (CONTEXT.md)
* **Profile-Only Parameter Governance:** พารามิเตอร์ AI model (temperature, top_p, keep_alive) ต้องมาจาก `ai_execution_profiles` row `ocr-extract` (ADR-036)
* **Security (ADR-016):** Path traversal hardening, no hardcoded secrets
* **Network Trust Boundary:** Server consolidation (ADR-041) ทำให้ Docker-internal isolation เป็นไปได้จริง
* **No Invented Orchestration:** ห้ามสร้าง `VramMutexService`, `GpuTaskQueue`, `PromptBuilderService` ใหม่ — ใช้ existing services/Active Prompt ตาม ADR-008, ADR-029/037
* **ADR-023A Boundary:** AI sidecar ห้ามเข้าถึง DB/storage โดยตรง
---
## 🏛️ Decisions
### D1: Sidecar as Pure Compute Worker
Sidecar ทำหน้าที่เป็น compute worker เท่านั้น — orchestration, parameter governance, และ business logic อยู่ใน backend (existing services)
- **Reject:** การสร้าง `PromptBuilderService`, `OcrNoiseFilterService`, `OcrOrchestratorService` ใหม่ (Qwen plan)
- **Fast-path decision** (PyMuPDF chars > 100 → fast path): คงไว้ใน sidecar
- **Page range calculation:** ย้ายไป backend
- **Engine selection:** ไม่ต้องมีแล้ว — ใช้ np-dms-ocr ตัวเดียว (Typhoon OCR)
- **systemPrompt validation** (ตรวจสอบ placeholders เช่น `{{ocr_text}}`): backend
### D2: Remove /normalize Endpoint
- **ตัด /normalize endpoint** ออกจาก sidecar
- **ใช้แค่ np-dms-ocr (OCR)** เท่านั้น — sidecar ไม่รองรับ Thai normalization
- ThaiPreprocessProcessor ไม่มีการใช้งาน — ไม่ต้องแก้ไข backend
### D3: Async I/O + Lifespan + Shared AsyncClient
- `process_ocr``async def`
- ใช้ `httpx.AsyncClient` shared ผ่าน lifespan context manager
- เปลี่ยนจาก `@app.on_event("startup")` เป็น `@asynccontextmanager` lifespan
- Load models ผ่าน `asyncio.to_thread` เพื่อไม่ block startup
### D4: keep_alive via calculate_ocr_residency() (Lazy, ADR-036 Gap-2)
- Wire `calculate_ocr_residency(active_profile)` เข้า `process_ocr`
- ไม่ใช้ fixed value (Claude 300, Qwen 0/10m)
- **ไม่รับ** explicit `options_override["keep_alive"]` จาก backend — keep_alive เป็น lazy resource param ที่คำนวณณ process time เท่านั้น (ADR-036 Gap-2)
- **Reject:** การลบ `vram_monitor.py` / `residency_policy.py`
### D5: Retain vram_monitor + CPU-Fallback for /embed, /rerank
- **Reject:** การบังคับ BGE-M3 + Reranker GPU-resident ถาวร
- **Keep:** Dynamic CPU/GPU selection ผ่าน `.to(device)` logic
- เป็นการ implement LLM-First GPU Ownership + CPU Fallback Retrieval
### D6: Remove Hardcoded Default Key; Auth = Network Isolation (2-Phase)
- **Phase 1** (ก่อน consolidation): ลบ hardcoded default `OCR_SIDECAR_API_KEY` — fail-fast ถ้า env missing
- **Phase 2** (หลัง consolidation): **Supersedes ADR-033 §7** — ลบ `X-API-Key` validation จาก sidecar endpoints และ backend send-side
- **Network Isolation:** ตรวจสอบผ่าน Docker-internal network (post-consolidation) หรือ VLAN/firewall ACL (interim cross-host)
- **Sequencing:** ลบ `X-API-Key` เฉพาะเมื่อ ADR-041 cutover เสร็จ (single Docker host)
- **Interim Period:** ระหว่าง Phase 1 และ Phase 2, sidecar และ backend ต้อง **ยังคง** validate และส่ง `X-API-Key`
- Rotate leaked key ก่อน cutover
### D7: Path Canonicalization + Base-Path Whitelist on /ocr
- Canonicalize `pdfPath` ผ่าน `os.path.abspath()` + `os.path.realpath()`
- Whitelist base path = `OCR_SIDECAR_UPLOAD_BASE` (CIFS mount base)
- Reject paths ที่ไม่ได้อยู่ภายใต้ base path → 403 Forbidden
### D8: Runtime Params from Job Snapshot (ocr-extract row)
- **Backend** resolve params จาก `ai_execution_profiles` (row `ocr-extract` สำหรับ OCR, profile สำหรับ LLM)
- **Backend** ส่ง params (`temperature`, `top_p`, `repeat_penalty`, `max_tokens`) ไปให้ sidecar
- **Sidecar** รับ params จาก backend แล้วส่งต่อไป Ollama (ในทุกครั้งที่ load/generate)
- ห้าม hardcode defaults ใน sidecar
- Modfile ทำหน้าที่เป็น last-resort fallback เท่านั้น
- Align กับ ADR-036 Profile-Only Parameter Governance
### D9: DMS Tags + SystemPrompt from Active Prompt
- **Backend** resolve systemPrompt จาก Active Prompt ใน `ai_prompts` (ADR-029/037)
- **Backend** resolve DMS extraction tags (`<document_number>`, `<document_date>`, `<received_date>`) จาก Active Prompt
- **Backend** ส่งทั้ง systemPrompt และ DMS tags ไปให้ sidecar
- **Sidecar** รับ systemPrompt และ DMS tags จาก backend แล้วส่งต่อไป Ollama (ในทุกครั้งที่ load/generate)
- **Reject:** การสร้าง `PromptBuilderService` ใหม่เป็น prompt authority
---
## 📋 Implementation Tasks
### Phase 1 — ก่อน ADR-041 Consolidation (ยังคง X-API-Key)
| Task ID | Component | Summary | Status |
| :--- | :--- | :--- | :--- |
| T001 | Sidecar | Remove hardcoded default API key (fail-fast if env missing) | Pending |
| T002 | Sidecar | Fix mutable default arg `options_override={}` | Pending |
| T003 | Sidecar | Remove duplicate `import tempfile` | Pending |
| T004 | Sidecar | Refactor to async I/O + shared AsyncClient | Pending |
| T005 | Sidecar | Replace `@app.on_event("startup")` with lifespan | Pending |
| T006 | Sidecar | Wire `calculate_ocr_residency()` into `process_ocr` | Pending |
| T007 | Sidecar | Path canonicalization + base-path whitelist on `/ocr` | Pending |
| T008 | Sidecar | Remove hardcoded runtime params (use from job snapshot) | Pending |
| T009 | Sidecar | Receive systemPrompt + DMS tags from backend, pass to Ollama | Pending |
| T010 | Sidecar | Remove `/normalize` endpoint (D2) | Pending |
| T011 | Backend | Send runtime params from `ai_execution_profiles` snapshot to sidecar | Pending |
| T012 | Backend | Wire Active Prompt injection for DMS tags + systemPrompt | Pending |
| T013 | Tests | Pytest for path-traversal (403) | Pending |
| T014 | Tests | Unit check for residency wiring | Pending |
### Phase 2 — หลัง ADR-041 Consolidation (ลบ X-API-Key)
| Task ID | Component | Summary | Status |
| :--- | :--- | :--- | :--- |
| T016 | Sidecar | Remove `X-API-Key` validation from endpoints | Pending (ADR-041 cutover) |
| T017 | Backend | Remove `X-API-Key` send-side in `OcrService` | Pending (ADR-041 cutover) |
| T018 | Backend | Remove `X-API-Key` send-side in `SandboxOcrEngineService` | Pending (ADR-041 cutover) |
---
## 📋 Consequences
### Positive
* **OOM Safety Retained:** รักษา Adaptive OCR Residency + CPU Fallback Retrieval — ป้องกัน VRAM exhaustion
* **Spec-Consistent:** สอดคล้องกับ ADR-036, ADR-029/037, CONTEXT.md
* **Smaller Sidecar Surface:** Pure compute worker — ไม่มี business logic หรือ parameter governance
* **Security Hardened:** Path traversal fix, no hardcoded secrets
* **Performance:** Async I/O ลด blocking, shared AsyncClient ลด connection overhead
### Negative
* **Lose Defense-in-Depth Auth:** ลบ `X-API-Key` ทำให้ขึ้นอยู่กับ network isolation เท่านั้น — mitigated โดย ACL/bridge network
* **Cross-Host Firewall Rule Mandatory:** ใน topology ปัจจุบัน (ก่อน consolidation) ต้องมี VLAN/firewall ACL เป็น interim constraint
* **Migration Complexity:** Sequencing ของ auth removal ต้อง sync กับ ADR-041 cutover
---
## 🚫 Out of Scope (Future ADR)
* 1-page-1-request horizontal scaling rework (Qwen 2.7) — ต้องการ separate spec + load evidence
* OpenTelemetry/Prometheus/Grafana observability (Qwen 4.44.5) — separate ticket
* **/normalize endpoint** — ตัดออกจาก sidecar แล้ว (D2); ThaiPreprocessProcessor ไม่มีการใช้งาน
---
## 🔄 Rollback Plan
* Revert `app.py` ไปเวอร์ชันก่อน refactor
* Restore `X-API-Key` send-side ใน `OcrService` และ `SandboxOcrEngineService`
* Re-pin `keep_alive` default เป็น `0` ใน `process_ocr`
* Restore hardcoded runtime params (ถ้าต้องการ emergency fallback)
---
## 📝 Verification Plan
1. Confirm backend send-side `X-API-Key` locations:
- `backend/src/modules/ai/services/ocr.service.ts`
- `backend/src/modules/ai/services/sandbox-ocr-engine.service.ts`
2. Confirm `calculate_ocr_residency` ไม่ถูกเรียกใช้ใน `app.py` (grep) ก่อน claim gap
3. ✅ ยืนยันแล้ว: ไม่มี consumer ใดใช้ `/normalize` endpoint (grep ไม่พบใน backend)
4. Pytest สำหรับ path-traversal (expect 403)
5. Unit test สำหรับ residency wiring
@@ -0,0 +1,336 @@
<!-- File: specs/06-Decision-Records/ADR-041-server-consolidation.md -->
<!-- Change Log
- 2026-06-20: Created initial ADR-041 documenting server consolidation decision.
- Co-locate all services on single Docker host (Ryzen 5 5600 / 32GB / RTX 5060 Ti 16GB).
- QNAP remains NAS for uploads/permanent storage via CIFS.
- Enables ADR-040 network-only auth for sidecar via Docker-internal isolation.
-->
# ADR-041: Single-Host Server Consolidation
**Status:** Proposed
**Date:** 2026-06-20
**Related Documents:**
- [ADR-040: OCR Sidecar Refactor](./ADR-040-ocr-sidecar-refactor.md)
- [ADR-016: Security & Authentication](./ADR-016-security-authentication.md)
- [ADR-023A: Unified AI Architecture](./ADR-023A-unified-ai-architecture.md)
- [ADR-034: AI Model Change](./ADR-034-AI-model-change.md)
- [CONTEXT.md](../../00-overview/CONTEXT.md)
---
## 🎯 Context and Problem Statement
### Current Architecture
ปัจจุบัน LCBP3-DMS กระจาย services ไว้บนหลายเครื่อง:
| Service | Host | Hardware | Network |
|---------|------|----------|---------|
| Ollama (np-dms-ai, np-dms-ocr, nomic-embed) | Desk-5439 | RTX 4060 Ti 16GB | VLAN 10 (192.168.10.100) |
| OCR Sidecar (FastAPI) | Desk-5439 | Same as above | VLAN 10 (192.168.10.100) |
| Backend (NestJS) | QNAP NAS | - | VLAN 10 (192.168.10.8) |
| Frontend (Next.js) | QNAP NAS | - | VLAN 10 (192.168.10.8) |
| Redis | QNAP NAS | - | VLAN 10 (192.168.10.8) |
| MariaDB | QNAP NAS | - | VLAN 10 (192.168.10.8) |
| Elasticsearch | QNAP NAS | - | VLAN 10 (192.168.10.8) |
| File Storage | QNAP NAS | - | CIFS share `np-dms-as` |
### Problems Identified
1. **Cross-Host Trust Boundary:** Backend ↔ sidecar/Ollama ผ่าน LAN (VLAN 10) — ต้องพึ่ง VLAN/firewall ACL สำหรับ isolation (ADR-040 §4)
2. **Management Complexity:** Services กระจายบน 2 hosts → deployment, monitoring, troubleshooting ซับซ้อน
3. **GPU Resource Fragmentation:** Desk-5439 มี GPU แต่ CPU/RAM น้อย → ไม่สามารถรัน backend ได้
4. **Network Latency:** Backend ↔ Ollama ผ่าน LAN เพิ่ม latency สำหรับ AI inference
5. **Hardware Underutilization:** QNAP NAS มี CPU/RAM แต่ไม่มี GPU → ไม่สามารถรัน AI models ได้
### New Hardware
มีเซิร์ฟเวอร์ใหม่พร้อมใช้งาน:
- **CPU:** Ryzen 5 5600 (6 cores / 12 threads)
- **RAM:** 32GB DDR4
- **GPU:** RTX 5060 Ti 16GB
- **Storage:** SSD (OS) + HDD (data)
---
## ⚙️ Decision Drivers
* **Simplify Architecture:** ลดจำนวน hosts จาก 2 → 1
* **Enable Docker-Internal Isolation:** Sidecar + backend อยู่บน Docker bridge เดียวกัน → network auth จริง (ADR-040 D5)
* **Better Resource Utilization:** Single host มีทั้ง CPU, RAM, GPU ในเครื่องเดียว
* **Reduce Network Latency:** Backend ↔ Ollama ผ่าน localhost แทน LAN
* **Maintain Data Separation:** QNAP ยังคงเป็น NAS สำหรับ file storage
---
## 🏛️ Decisions
### D1: Co-locate All Services on Single Docker Host
ย้าย services ทั้งหมดไปรันบนเซิร์ฟเวอร์ใหม่:
- Ollama (np-dms-ai, np-dms-ocr, nomic-embed)
- OCR Sidecar (FastAPI)
- Backend (NestJS)
- Frontend (Next.js)
- Redis
- MariaDB
- Elasticsearch
**Retire Desk-5439** หลัง cutover สำเร็จ
### D2: ASUSTOR as Primary NAS, QNAP as Backup
QNAP (192.168.10.8) ลดบทบาทเป็น backup server เท่านั้น
ASUSTOR (192.168.10.9) เป็น Primary NAS สำหรับ:
- Upload temp storage (`/data/uploads/temp`)
- Permanent file storage (`/data/uploads/permanent`)
- CIFS share `np-dms-as` ถูก mount บน new host ผ่าน:
- `/mnt/uploads/temp``//192.168.10.9/np-dms-as/data/uploads/temp`
- `/mnt/uploads/permanent``//192.168.10.9/np-dms-as/data/uploads/permanent`
### D3: Docker-Internal Network Only for Sidecar/Ollama
- Sidecar และ Ollama **ไม่ publish ports ไป LAN** (ใช้ `expose` แทน `ports`)
- Services อยู่บน internal Docker bridge network (`dms-internal`)
- Backend ติดต่อ sidecar/Ollama ผ่าน `http://sidecar:8765` และ `http://ollama:11434` (service names)
- Frontend ติดต่อ backend ผ่าน `http://backend:3000`
- เฉพาะ Frontend และ Backend เท่านั้นที่ publish ports ไป LAN (80, 443, 3000)
**Enables ADR-040 D5:** Network isolation ผ่าน Docker-internal bridge → ลบ `X-API-Key` ได้จริง
### D4: GPU VRAM Management Reinforced
RTX 5060 Ti 16GB ต้องรองรับ:
- `np-dms-ai` (Typhoon-2.5 ~78B) ~68GB
- `np-dms-ocr` (Typhoon OCR) ~5GB
- `nomic-embed-text` ~0.5GB
- BGE-M3 + Reranker (ถ้า GPU-resident) ~4.5GB
- CUDA overhead ~1.5GB
**Total ≈ 15.5GB → OOM risk หาก load พร้อมกันทั้งหมด**
**Mandatory:**
- ADR-040 D3 (Adaptive OCR Residency via `calculate_ocr_residency()`)
- ADR-040 D4 (CPU Fallback Retrieval for embed/rerank)
- LLM-First GPU Ownership (CONTEXT.md)
- ไม่บังคับ BGE+Reranker GPU-resident ถาวร
### D5: RAM Budget Considerations
32GB RAM ต้องรองรับ:
- Node.js (Frontend) ~500MB
- NestJS (Backend) ~12GB
- MariaDB ~48GB (ขึ้นกับ dataset size)
- Redis ~500MB
- Elasticsearch ~24GB (ขึ้นกับ index size)
- Python (Sidecar) ~500MB
- Ollama ~12GB
- BGE/Reranker CPU-fallback tensors ~24GB
**Action Items:**
- Size DB/ES/Redis memory limits ก่อน cutover
- Monitor RAM usage หลัง cutover
- พิจารณา swap space ถ้าจำเป็น
### D6: Single Point of Failure (SPOF) Mitigation
Single host = SPOF risk
**Mitigation:**
- Regular backup ของ database และ file storage (QNAP)
- Disaster recovery plan สำหรับ hardware failure
- พิจารณา cold standby หรือ failover strategy ในอนาคต
---
## 📋 Implementation Tasks
| Task ID | Phase | Summary | Status |
| :--- | :--- | :--- | :--- |
| T001 | Provision | Install Docker + Docker Compose on new host | Pending |
| T002 | Provision | Mount CIFS share from ASUSTOR to `/mnt/uploads` | Pending |
| T003 | Deploy | Create `docker-compose.yml` for new host topology | Pending |
| T004 | Deploy | Configure internal bridge network (`dms-internal`) | Pending |
| T005 | Deploy | Deploy services (Ollama, sidecar, backend, frontend, Redis, DB, ES) | Pending |
| T006 | Migrate | Migrate MariaDB data from QNAP to new host | Pending |
| T007 | Migrate | Migrate Elasticsearch indices from QNAP to new host | Pending |
| T008 | Cutover | Update DNS/load balancer to point to new host | Pending |
| T009 | Cutover | Run smoke tests on new host | Pending |
| T010 | ADR-040 | Remove `X-API-Key` from sidecar + backend (ADR-040 D5) | Pending |
| T011 | Cleanup | Stop services on QNAP (QNAP becomes backup server) | Pending |
| T012 | Cleanup | Retire Desk-5439 | Pending |
---
## 📋 Target docker-compose Layout (Draft)
```yaml
version: '3.8'
networks:
dms-internal:
driver: bridge
dms-frontend:
driver: bridge
services:
# GPU Services (internal-only, no LAN publish)
ollama:
image: ollama/ollama:latest
container_name: lcbp3-ollama
restart: unless-stopped
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
- ollama_models:/root/.ollama
networks:
- dms-internal
expose:
- "11434"
environment:
- OLLAMA_KEEP_ALIVE=-1
ocr-sidecar:
build:
context: ./specs/04-Infrastructure-OPS/04-00-docker-compose/New-Host/ocr-sidecar
container_name: lcbp3-ocr-sidecar
restart: unless-stopped
volumes:
- asustor_uploads:/mnt/uploads:ro # Read-only CIFS mount from ASUSTOR
networks:
- dms-internal
expose:
- "8765"
depends_on:
- ollama
environment:
- OLLAMA_API_URL=http://ollama:11434
- OCR_SIDECAR_UPLOAD_BASE=/mnt/uploads
# Backend Services (internal-only)
backend:
build:
context: ./backend
container_name: lcbp3-backend
restart: unless-stopped
volumes:
- asustor_uploads:/app/uploads:ro
networks:
- dms-internal
- dms-frontend
expose:
- "3000"
depends_on:
- ollama
- ocr-sidecar
- redis
- mariadb
- elasticsearch
environment:
- OCR_API_URL=http://ocr-sidecar:8765
- OLLAMA_API_URL=http://ollama:11434
# Frontend (LAN publish)
frontend:
build:
context: ./frontend
container_name: lcbp3-frontend
restart: unless-stopped
networks:
- dms-frontend
ports:
- "3000:3000"
depends_on:
- backend
# Data Services
redis:
image: redis:7-alpine
container_name: lcbp3-redis
restart: unless-stopped
networks:
- dms-internal
volumes:
- redis_data:/data
mariadb:
image: mariadb:10.11
container_name: lcbp3-mariadb
restart: unless-stopped
networks:
- dms-internal
volumes:
- mariadb_data:/var/lib/mysql
environment:
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD}
- MYSQL_DATABASE=lcbp3
elasticsearch:
image: elasticsearch:8.11.0
container_name: lcbp3-elasticsearch
restart: unless-stopped
networks:
- dms-internal
volumes:
- es_data:/usr/share/elasticsearch/data
environment:
- discovery.type=single-node
- xpack.security.enabled=false
volumes:
ollama_models:
asustor_uploads:
driver: local
driver_opts:
type: cifs
o: "username=${ASUSTOR_USER},password=${ASUSTOR_PASS},vers=3.0,uid=0,gid=0"
device: "//192.168.10.9/np-dms-as/data/uploads"
redis_data:
mariadb_data:
es_data:
```
---
## 📋 Consequences
### Positive
* **Simplified Architecture:** Single host → easier deployment, monitoring, troubleshooting
* **True Network Isolation:** Docker-internal bridge enables ADR-040 D5 (network-only auth)
* **Reduced Latency:** Backend ↔ Ollama ผ่าน localhost
* **Better Resource Utilization:** Single host มีทั้ง CPU, RAM, GPU
* **Data Separation Maintained:** ASUSTOR เป็น Primary NAS → data แยกจาก compute; QNAP เป็น backup server
### Negative
* **SPOF Risk:** Single host = single point of failure
* **RAM Pressure:** 32GB ต้องรองรับ services ทั้งหมด + CPU-fallback tensors
* **Migration Complexity:** ต้อง migrate DB + ES + file paths
* **GPU VRAM Pressure:** 16GB ต้องอาศัย adaptive residency + CPU fallback
---
## 🔄 Rollback Plan
1. Stop services บน new host
2. Restore services บน QNAP (backend, frontend, Redis, DB, ES)
3. Restore services บน Desk-5439 (Ollama, sidecar)
4. Revert DNS/load balancer ไป QNAP
5. Update CIFS mount กลับไป ASUSTOR (192.168.10.9) บน QNAP
6. Restore `X-API-Key` ใน sidecar + backend (ADR-040 rollback)
---
## 📝 Verification Plan
1. Smoke tests บน new host:
- Backend health check
- Frontend accessible via LAN
- OCR endpoint functional
- AI inference functional
- File upload/download via CIFS
2. Monitor RAM/VRAM usage 2448 hours หลัง cutover
3. Verify ADR-040 D5 (network-only auth) ทำงานได้จริง
4. Verify ADR-040 D3/D4 (adaptive residency + CPU fallback) ทำงานได้จริง