# Nginx 기반 로빙 컨테이너 아키텍처 설계 ## 목차 1. [배경 및 문제점](#배경-및-문제점) 2. [Docker 이미지 최적화](#docker-이미지-최적화) 3. [Nginx 리버스 프록시 아키텍처](#nginx-리버스-프록시-아키텍처) 4. [컨테이너 동적 관리](#컨테이너-동적-관리) 5. [비용 분석](#비용-분석) 6. [Docker 유료화 현황](#docker-유료화-현황) 7. [아키텍처 성능 비교](#아키텍처-성능-비교) 8. [로빙 컨테이너 아키텍처 설계](#로빙-컨테이너-아키텍처-설계) 9. [예제 구현](#예제-구현) --- ## 배경 및 문제점 ### 초기 상황 - 각 폴더(docs, frontend, api-base, test_api, test_meta-skill, test_front)의 git 상태 확인 및 동기화 완료 - api-base 폴더에서 새로운 변경사항 발견: - Dockerfile 및 docker-compose.yml 추가 - static/test.html 파일 추가 - requirements.txt 업데이트 ### 발견된 문제점 - **Docker 이미지 용량 과다**: 1.54GB로 예상보다 크게 빌드됨 - **고정 파라미터**: run.py에서 모든 설정값이 하드코딩됨 - **확장성 문제**: 컨테이너 수 증가 시 nginx.conf 수동 관리 필요 --- ## Docker 이미지 최적화 ### 문제 분석 기존 Dockerfile에서 불필요한 패키지 설치로 인한 용량 증가: ```dockerfile # 기존 문제가 있던 Dockerfile FROM python:3.11-slim RUN apt-get update && apt-get install -y \ gcc \ postgresql-client \ && rm -rf /var/lib/apt/lists/* ``` ### gcc 필요성 검증 requirements.txt 분석 결과: ```text fastapi>=0.104.0 uvicorn>=0.24.0 psycopg2-binary>=2.9.0 # 바이너리 버전 사용 chromadb>=0.4.0 pymupdf>=1.23.0 python-jose[cryptography]>=3.3.0 # ... 기타 패키지들 ``` **테스트 결과**: gcc 없이도 모든 패키지가 정상 설치됨 - 모든 패키지가 바이너리 wheel로 제공 - psycopg2-binary 사용으로 컴파일 불필요 ### 최적화된 Dockerfile ```dockerfile FROM python:3.11-slim WORKDIR /app # 필수 런타임 의존성만 설치 RUN apt-get update && apt-get install -y --no-install-recommends \ && rm -rf /var/lib/apt/lists/* # requirements 먼저 복사 (캐시 활용) COPY requirements.txt . RUN pip install --no-cache-dir --no-compile -r requirements.txt # 애플리케이션 코드 복사 COPY . . # 비루트 사용자 생성 RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser # 포트 노출 EXPOSE 8000 # 환경변수로 설정 관리 ENV HOST=0.0.0.0 ENV PORT=8000 ENV RELOAD=false ENV LOG_LEVEL=info # 파라미터화된 실행 CMD ["sh", "-c", "python run.py --host $HOST --port $PORT"] ``` ### run.py 파라미터 외부화 기존 하드코딩된 설정: ```python # 기존 run.py if __name__ == "__main__": uvicorn.run( "app.main:app", host="0.0.0.0", # 고정값 port=8000, # 고정값 reload=True, # 고정값 log_level="info" # 고정값 ) ``` 개선된 환경변수 기반 설정: ```python import os import uvicorn from app.main import app if __name__ == "__main__": uvicorn.run( "app.main:app", host=os.getenv("HOST", "0.0.0.0"), port=int(os.getenv("PORT", "8000")), reload=os.getenv("RELOAD", "false").lower() == "true", log_level=os.getenv("LOG_LEVEL", "info") ) ``` --- ## Nginx 리버스 프록시 아키텍처 ### 기본 구조 ``` [Client Request] ↓ [Nginx (Reverse Proxy)] ↓ ↓ ↓ [Container A] [Container B] [Container C] (e.g. FastAPI) (e.g. Node.js) (e.g. LangChain) ``` ### Nginx 설정 예시 ```nginx http { upstream app1 { server container-a:8000; } upstream app2 { server container-b:8001; } server { listen 80; location /app1/ { proxy_pass http://app1/; } location /app2/ { proxy_pass http://app2/; } } } ``` ### Docker Compose 구성 ```yaml version: '3.8' services: nginx: image: nginx ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - container-a - container-b container-a: build: ./app1 expose: - "8000" container-b: build: ./app2 expose: - "8001" ``` ### Nginx 컨테이너화 필요성 **컨테이너로 감싸는 이유:** - **이식성**: 어디서든 동일한 환경으로 실행 가능 - **버전 고정**: Nginx 이미지 버전 명시로 예기치 않은 업데이트 방지 - **구성 분리**: nginx.conf 등 설정 파일을 외부 볼륨으로 관리 - **CI/CD 통합**: 자동 배포 및 테스트 환경 구성 용이 - **DevOps 표준화**: 다른 마이크로서비스와 동일한 방식으로 관리 --- ## 컨테이너 동적 관리 ### nginx.conf 동적 갱신 문제 **문제**: 기본 nginx는 정적 설정 파일 기반으로 컨테이너 수 증가 시 자동 갱신 불가 **해결 방법:** #### 1. Docker + Nginx + Template 갱신 도구 ```bash # nginx-proxy 사용 예시 docker run -d -p 80:80 \ -v /var/run/docker.sock:/tmp/docker.sock:ro \ jwilder/nginx-proxy ``` #### 2. Traefik 사용 (권장) - Docker 레이블 기반 동적 서비스 탐지 - 자동 라우팅 처리 - Let's Encrypt 자동 적용 ```yaml services: traefik: image: traefik:v2.10 command: - "--providers.docker=true" - "--entrypoints.web.address=:80" ports: - "80:80" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro ``` ### 조건 기반 컨테이너 자동 제어 **시나리오**: 로빙 레벨업, 스킬 업데이트 시 자동 컨테이너 관리 **구현 방식:** ```python import docker def handle_level_up(robeing_id, new_level): client = docker.from_env() # 기존 컨테이너 중단 old_container = client.containers.get(f"robeing-{robeing_id}") old_container.stop() old_container.remove() # 새 레벨에 맞는 컨테이너 시작 client.containers.run( f"robeing:level-{new_level}", name=f"robeing-{robeing_id}", detach=True, volumes={ f"robeing-{robeing_id}-data": { "bind": "/app/data", "mode": "rw" } } ) ``` **자동화 조건 예시:** - 로빙 레벨 5 도달 → GPT-4o 컨테이너 시작 - 스킬X 업그레이드 → 기존 컨테이너 중단 후 새 이미지로 재배포 - PDF 스킬 7일간 미사용 → 해당 컨테이너 자동 종료 --- ## 비용 분석 ### 컨테이너 자동화 비용 **비용 절감 전략:** - **이벤트 기반 실행**: 사용 시에만 컨테이너 활성화 - **레벨 제한**: 고성능 스킬은 상위 레벨에만 허용 - **경량 컨테이너**: FastAPI, uvicorn 기반 최소화 - **무료 티어 활용**: Hetzner, Railway, Supabase 등 **예산 범위 (월 기준, 1-3 스킬):** | 방식 | 예상 비용 | 비고 | |------|-----------|------| | AWS ECS Fargate + S3 | $3-8 | idle auto-off 조건 | | Supabase Edge Function | $0-5 | 기본 무료 + API 통제 | | Hetzner VPS + docker-compose | €4-6 | CX11 기준 | | Railway (1GB RAM) | $0-5 | 스킬 2-3개 | ### 컨테이너 호스팅 서비스 비용 **내부 전용 플랫폼** (로빙 등): - **인력**: 1-2명 (DevOps + 백엔드) - **비용**: 월 3-10만원 - **구성**: Hetzner VPS + Supabase + Docker **상업용 PaaS 플랫폼**: - **인력**: 5-10명 이상 - **비용**: 수천만원 이상 - **예시**: Railway, Render, Vercel 수준 --- ## Docker 유료화 현황 ### 정책 요약 (2024-2025) - **Docker Engine, Compose**: 계속 무료 - **Docker Desktop**: 연매출 $1천만 이상 기업만 유료 - **Docker Hub**: Free 요금제 Pull 제한 (6시간당 100회) ### 요금 구조 | 플랜 | 가격 | 대상 | |------|------|------| | Docker Personal | 무료 | 개인/소규모 상업용 | | Docker Pro | $9/월 | 개발자 | | Docker Team | $15/월 | 협업팀 | | Docker Business | $24/월 | 엔터프라이즈 | **결론**: 스타트업/MVP 단계에서는 비용 거의 없음 --- ## 아키텍처 성능 비교 ### 속도 측면 비교 | 항목 | Docker (컨테이너) | VM (가상머신) | 서버리스 (FaaS) | 베어메탈 | |------|------------------|---------------|-----------------|----------| | 시작 속도 | 매우 빠름 (초 단위) | 느림 (수십 초~수분) | 보통~빠름 (콜드스타트) | 느림 (OS 부팅) | | 처리 속도 (CPU) | 거의 네이티브급 | 약간 느림 | 느림~중간 | 최상 | | IO 성능 | 빠름 (호스트 공유) | 느림 (가상화 레이어) | 느림~중간 | 최상 | | 성능 일관성 | 중간~높음 | 높음 | 낮음 (콜드스타트) | 최상 | | 멀티 컨테이너 처리 | 뛰어남 | 복잡 | 불가 (함수 단위) | 복잡 | **결론**: 로빙 같은 동적 자원 할당과 상태 지속이 필요한 구조에서는 컨테이너 기반이 최적 --- ## 로빙 컨테이너 아키텍처 설계 ### 핵심 요구사항 - **레벨업 시 자원 증가**: 컨테이너 재시작으로 max 성능 향상 - **기억/코드 유지**: 볼륨 마운트로 데이터 지속성 보장 - **대시보드 연동**: 로그인, 상태, 아이템 권한, API 키 관리 - **동적 라우팅**: 컨테이너 변화에 따른 자동 프록시 업데이트 ### MVP 구성 요소 - **로빙 컨테이너**: 10개 - **리버스 프록시**: 1개 (Traefik) - **컨테이너 오케스트레이션**: Docker Swarm - **제어 백엔드**: FastAPI 기반 - **대시보드**: React/Vue 프론트엔드 ### 아키텍처 다이어그램 ``` [ User + 대시보드 ] ↓ REST/WebSocket [ Control API Server ] ↓ ↓ [ Docker Engine (Swarm) ] ←→ [ Redis / Postgres ] ↑ [ Reverse Proxy (Traefik) ] ↓ [ 로빙 컨테이너 1~10개 ] ``` ### 레벨업 동작 흐름 1. 로빙 경험치 증가 → 레벨업 판정 (PostgreSQL 반영) 2. Control API가 변화 감지 (cron, hook, event) 3. 기존 robeing-3 컨테이너 stop + remove 4. 더 큰 자원 설정으로 재시작 (`--memory`, `--cpu-shares` 증가) 5. 기존 `/data` 볼륨 마운트 유지 (기억/코드 보존) 6. Traefik이 자동 라우팅 업데이트 --- ## 예제 구현 ### Docker Swarm 기반 구성 ```yaml # docker-compose.yml version: '3.8' services: traefik: image: traefik:v2.10 command: - "--providers.docker=true" - "--entrypoints.web.address=:80" - "--entrypoints.websecure.address=:443" - "--api.insecure=true" ports: - "80:80" - "8080:8080" # 관리 대시보드 volumes: - /var/run/docker.sock:/var/run/docker.sock:ro deploy: replicas: 1 resources: limits: cpus: '0.5' memory: '512M' control-api: image: ro-being/control-api:latest deploy: replicas: 1 environment: - DATABASE_URL=postgresql://user:pass@postgres:5432/robeing - REDIS_URL=redis://redis:6379/0 - DOCKER_SOCKET=/var/run/docker.sock volumes: - /var/run/docker.sock:/var/run/docker.sock networks: - traefik-public labels: - "traefik.http.routers.control-api.rule=Host(`api.robeing.local`)" postgres: image: postgres:15 environment: - POSTGRES_DB=robeing - POSTGRES_USER=user - POSTGRES_PASSWORD=pass volumes: - postgres_data:/var/lib/postgresql/data networks: - traefik-public redis: image: redis:7-alpine networks: - traefik-public # 로빙 컨테이너 템플릿 robeing-1: image: robeing:level-1 deploy: resources: limits: cpus: '0.5' memory: '256M' volumes: - robeing-1-memory:/app/memory - robeing-1-code:/app/code networks: - traefik-public labels: - "traefik.http.routers.robeing1.rule=Host(`robeing1.local`)" - "traefik.http.services.robeing1.loadbalancer.server.port=8000" networks: traefik-public: external: true volumes: postgres_data: robeing-1-memory: robeing-1-code: ``` ### 로빙 컨테이너 제어 API ```python # control_api.py import docker from fastapi import FastAPI from sqlalchemy.orm import Session from database import get_db from models import Robeing app = FastAPI() client = docker.from_env() @app.post("/robeing/{robeing_id}/level-up") async def level_up_robeing(robeing_id: int, db: Session = Depends(get_db)): # 로빙 정보 조회 robeing = db.query(Robeing).filter(Robeing.id == robeing_id).first() if not robeing: raise HTTPException(404, "Robeing not found") # 레벨업 처리 new_level = robeing.level + 1 robeing.level = new_level db.commit() # 컨테이너 재시작 await restart_robeing_container(robeing_id, new_level) return {"message": f"Robeing {robeing_id} leveled up to {new_level}"} async def restart_robeing_container(robeing_id: int, level: int): container_name = f"robeing-{robeing_id}" try: # 기존 컨테이너 중지 및 제거 old_container = client.containers.get(container_name) old_container.stop() old_container.remove() except docker.errors.NotFound: pass # 레벨에 따른 자원 할당 memory_limit = f"{256 * level}m" cpu_quota = 100000 * level # CPU 할당량 # 새 컨테이너 시작 client.containers.run( image=f"robeing:level-{level}", name=container_name, detach=True, mem_limit=memory_limit, cpu_quota=cpu_quota, volumes={ f"robeing-{robeing_id}-memory": { "bind": "/app/memory", "mode": "rw" }, f"robeing-{robeing_id}-code": { "bind": "/app/code", "mode": "rw" } }, labels={ f"traefik.http.routers.robeing{robeing_id}.rule": f"Host(`robeing{robeing_id}.local`)", f"traefik.http.services.robeing{robeing_id}.loadbalancer.server.port": "8000" }, network="traefik-public" ) ``` ### 대시보드 연동 예시 ```python # dashboard_api.py @app.get("/dashboard/robeing/{robeing_id}/status") async def get_robeing_status(robeing_id: int): container_name = f"robeing-{robeing_id}" try: container = client.containers.get(container_name) stats = container.stats(stream=False) return { "id": robeing_id, "status": container.status, "cpu_usage": calculate_cpu_percent(stats), "memory_usage": stats['memory_stats']['usage'], "memory_limit": stats['memory_stats']['limit'], "level": get_robeing_level(robeing_id), "uptime": container.attrs['State']['StartedAt'] } except docker.errors.NotFound: return {"status": "not_found"} @app.post("/dashboard/robeing/{robeing_id}/action") async def control_robeing(robeing_id: int, action: str): container_name = f"robeing-{robeing_id}" container = client.containers.get(container_name) if action == "start": container.start() elif action == "stop": container.stop() elif action == "restart": container.restart() elif action == "pause": container.pause() elif action == "unpause": container.unpause() return {"message": f"Action {action} applied to robeing {robeing_id}"} ``` ### 스킬 기반 컨테이너 관리 ```yaml # 스킬 메타데이터 예시 (skill_metadata.yml) skills: - name: PDF_Parser level_required: 5 docker_image: robeing-skills/pdf-parser:latest resources: memory: "512m" cpu: "0.5" auto_shutdown: 3600 # 1시간 후 자동 종료 - name: Advanced_LLM level_required: 10 docker_image: robeing-skills/advanced-llm:latest resources: memory: "2g" cpu: "2.0" gpu_required: true ``` ### 실시간 모니터링 ```python # monitoring.py import asyncio import docker from fastapi import WebSocket @app.websocket("/ws/monitor/{robeing_id}") async def monitor_robeing(websocket: WebSocket, robeing_id: int): await websocket.accept() container_name = f"robeing-{robeing_id}" try: container = client.containers.get(container_name) # 실시간 stats 스트리밍 for stats in container.stats(stream=True): metrics = { "timestamp": datetime.now().isoformat(), "cpu_percent": calculate_cpu_percent(stats), "memory_usage": stats['memory_stats']['usage'], "memory_percent": calculate_memory_percent(stats), "network_rx": stats['networks']['eth0']['rx_bytes'], "network_tx": stats['networks']['eth0']['tx_bytes'] } await websocket.send_json(metrics) await asyncio.sleep(1) except docker.errors.NotFound: await websocket.send_json({"error": "Container not found"}) except Exception as e: await websocket.send_json({"error": str(e)}) ``` --- ## 결론 및 확장 방향 ### 핵심 성과 1. **Docker 이미지 최적화**: 1.54GB → 예상 400-500MB로 감소 2. **동적 컨테이너 관리**: 레벨업 시 자동 자원 재할당 3. **비용 효율성**: MVP 기준 월 10만원 이하로 운영 가능 4. **확장 가능한 구조**: Docker Swarm → Kubernetes 전환 가능 ### 향후 확장 계획 - **Kubernetes 전환**: StatefulSet + Custom Operator - **로빙 간 통신**: gRPC 또는 WebSocket 기반 메시지 연동 - **고급 모니터링**: Prometheus + Grafana 연동 - **AI 기반 자원 예측**: 사용 패턴 분석으로 선제적 스케일링 ### 검증된 기술 스택 - **컨테이너**: Docker + Docker Swarm - **프록시**: Traefik (동적 라우팅) - **백엔드**: FastAPI + PostgreSQL + Redis - **프론트엔드**: React/Vue (대시보드) - **모니터링**: Docker API + WebSocket 이 아키텍처를 통해 로빙의 레벨 기반 진화, 기억 지속성, 유연한 스킬 업데이트 요구사항을 모두 충족할 수 있으며, MVP에서 시작하여 단계적으로 확장 가능한 구조를 제공합니다.