1편 전체 목차
사전 준비 — Ubuntu · NVIDIA 드라이버 · Docker
Compose 파일을 실행하기 전에 반드시 완료해야 하는 3가지 선행 작업입니다. 이미 구성된 서버라면 아래 검증 명령어로 상태만 확인하세요.
✅ 선행 작업 체크리스트
| 단계 | 작업 | 검증 명령어 | 정상 결과 |
|---|---|---|---|
| 1 | Ubuntu 24.04 LTS Server 설치 + 기본 설정 | lsb_release -a | Ubuntu 24.04 출력 |
| 2 | NVIDIA 드라이버 설치 (nouveau 차단 후) | nvidia-smi | GPU 정보 + 드라이버 버전 출력 |
| 3 | Docker CE + Compose v2 설치 | docker compose version | Docker Compose v2.x 출력 |
| 4 | NVIDIA Container Toolkit 설치 + Docker 재시작 | docker run --rm --gpus all nvidia/cuda:12.6.0-base-ubuntu22.04 nvidia-smi | 컨테이너 내부에서 GPU 출력 |
| 5 | daemon.json 설정 (UFW 우회 차단, nvidia runtime 기본) | cat /etc/docker/daemon.json | iptables:false, default-runtime:nvidia 확인 |
🔢 CUDA ↔ 드라이버 최소 요구사항 (GPU별 요약)
| GPU | 최소 드라이버 | 권장 드라이버 | 권장 CUDA | PyTorch |
|---|---|---|---|---|
| RTX 3060/3070/3080/3090 (Ampere) | 450.80+ | 560.x | CUDA 12.4~12.6 | 2.5.x / 2.6.x |
| RTX 4060/4070/4080/4090 (Ada) | 525.60+ | 560.x | CUDA 12.4~12.6 | 2.5.x / 2.6.x |
| RTX 5080/5090 (Blackwell) | 570.x+ | 570.x+ | CUDA 12.8 | 2.6.x+ |
Ollama, ComfyUI, vLLM 등은 CUDA 런타임을 Docker 이미지 안에 포함합니다. 호스트에는 NVIDIA 드라이버 + NVIDIA Container Toolkit만 있으면 됩니다. 호스트에서 직접 Python AI 스크립트를 실행할 때만 CUDA Toolkit 전체가 필요합니다.
전체 컨테이너 한눈에 보기 — 역할·필요성·제외 조건
아래 카드는 이 Compose 파일에 포함된 모든 서비스입니다. 왜 써야 하는지, 언제 빼도 되는지를 먼저 파악하고 자신의 환경에 맞게 선택하세요.
내부 URL:
http://ai-ollama:11434브라우저:
http://서버IP:3000API Key로 접근 제어 필수
브라우저:
http://서버IP:9000docker compose pull && docker compose up -d를 매번 수동으로 실행할 필요가 없습니다.서비스 연동 구조도 — 누가 누구와 통신하는가
아래 구조도는 각 서비스가 어떤 방향으로 데이터를 주고받는지 보여줍니다. 화살표 방향은 요청의 방향(A → B: A가 B를 호출)입니다.
🔀 채팅 흐름 (사용자 질문 → 답변 과정)
🎙️ 음성 흐름 (마이크 → 음성 답변 과정)
🗄️ 데이터 저장 흐름
📊 모니터링 흐름
🌐 네트워크 레이어 분리
| 네트워크 | 목적 | 포함 서비스 | 외부 접근 |
|---|---|---|---|
ai-internal-net | 서비스 간 내부 통신 전용 | 모든 서비스 | ❌ 차단 |
ai-external-net | 외부 공개가 필요한 서비스 | open-webui, n8n, nginx-pm | ✅ Nginx 통해서만 |
| host network | 시스템 메트릭 수집 정확도 | node-exporter만 | ❌ 내부만 |
통합 Docker Compose — 초주석 완전판
모든 서비스를 하나의 파일로 관리합니다. YAML 주석이 각 서비스·설정·포트의 이유를 완전하게 설명하므로 파일 자체가 학습 문서입니다. Docker Compose Profiles로 필요한 서비스만 선택 실행합니다.
##═══════════════════════════════════════════════════════════════════════════ ## AI 서버 통합 Docker Compose — 완전 주석판 ## 작성: agibop.com · 업데이트: 2026년 5월 ## ## [실행 방법] ## # 핵심 서비스만 (권장 시작 구성) ## docker compose --profile core --profile database --profile manage up -d ## ## # 이미지 생성 추가 ## docker compose --profile core --profile database \ ## --profile manage --profile imaging up -d ## ## # 전체 실행 (전부 필요한 경우) ## docker compose \ ## --profile core --profile database --profile manage \ ## --profile automation --profile imaging --profile voice \ ## --profile monitoring up -d ## ## [프로파일 목록] ## core : Ollama + Open WebUI + Pipelines + SearXNG (LLM 핵심) ## database : Qdrant + PostgreSQL + Redis (데이터 레이어) ## manage : Portainer + Nginx PM + Watchtower + Uptime (관리 도구) ## automation : n8n + Dify (자동화) ## imaging : ComfyUI + A1111 (이미지 생성) ## voice : Whisper + Kokoro TTS (음성 AI) ## monitoring : Prometheus + Grafana + 3 Exporters (모니터링) ##═══════════════════════════════════════════════════════════════════════════ name: ai-server # 스택 이름 — 모든 리소스(볼륨·네트워크)의 접두사가 됨 ##─────────────────────────────────────────────────────────────────────────── ## YAML 앵커 (Anchors) — 반복되는 설정을 한 곳에서 관리 ## &이름으로 정의하고, *이름으로 참조 (DRY 원칙) ##─────────────────────────────────────────────────────────────────────────── # GPU 전체 사용 — Ollama처럼 GPU 전체를 독점하는 서비스용 x-gpu-all: &gpu-all deploy: resources: reservations: devices: - driver: nvidia count: all # 모든 GPU 사용. 1개만 쓰려면 count:1 capabilities: [gpu] # GPU 0번 고정 — LLM과 이미지 생성을 GPU별 분리할 때 사용 # 멀티 GPU 서버에서 Ollama는 GPU 0, ComfyUI는 GPU 1로 분리 가능 x-gpu-0: &gpu-0 deploy: resources: reservations: devices: - driver: nvidia device_ids: ['0'] # GPU 0번만. 싱글 GPU 서버에서는 gpu-all과 동일 capabilities: [gpu] # GPU 1번 고정 — 듀얼 GPU 서버에서 이미지 생성용으로 분리 x-gpu-1: &gpu-1 deploy: resources: reservations: devices: - driver: nvidia device_ids: ['1'] # GPU 1번. 싱글 GPU 서버라면 gpu-0으로 교체 capabilities: [gpu] # 컨테이너 재시작 정책 — 서버 재부팅 시 자동 시작, 수동 stop만 제외 x-restart: &restart restart: unless-stopped # 로그 설정 — 파일이 무한히 커지는 것을 방지 x-logging: &logging logging: driver: json-file options: max-size: "100m" # 파일당 최대 100MB max-file: "3" # 최대 3개 파일 로테이션 (총 최대 300MB/서비스) services: ##═══════════════════════════════════════════════════════════════════════════ ## 🧠 CORE 프로파일 — LLM 추론 핵심 스택 ## 이 프로파일이 AI 서버의 최소 실행 단위입니다. ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # OLLAMA — LLM 추론 엔진 (AI 서버의 심장) # ───────────────────────────────────────── # 역할: GPU에서 AI 모델(Llama, Qwen, Gemma 등)을 실행하는 백엔드 엔진 # 왜 쓰는가: # - Open WebUI의 채팅 응답, n8n AI 노드, RAG 임베딩 생성 모두 담당 # - API 호환(OpenAI 형식)이라 기존 코드 그대로 로컬 전환 가능 # - GPU 자동 감지, Flash Attention, 멀티 모델 동시 로딩 지원 # 제외 조건: OpenAI/Claude 외부 API만 쓰고 로컬 LLM이 필요 없을 때 # 연동: ← open-webui(채팅), ← n8n(자동화), ← dify(AI앱), ← pipelines #──────────────────────────────────────────────────────────────────────── ollama: image: ollama/ollama:latest container_name: ai-ollama profiles: [core] <<: [*gpu-0, *restart, *logging] ports: - "11434:11434" # REST API 포트 — UFW로 외부 차단 권장 (내부망만 사용) volumes: # 볼륨 설명: # ollama_data: 다운로드한 AI 모델 파일 저장 (각 모델 4~40GB) # → 이 볼륨이 없으면 컨테이너 재시작마다 모델을 다시 다운로드해야 함 - ollama_data:/root/.ollama # 호스트의 모델 폴더를 읽기 전용으로 마운트 (외부 GGUF 파일 사용 시) # GGUF 파일을 DATA_ROOT/models에 넣으면 Ollama가 직접 로드 가능 - ${DATA_ROOT:-/home/ubuntu/ai-server/data}/models:/external_models:ro environment: # OLLAMA_NUM_PARALLEL: 동시에 처리할 요청 수 # 24GB VRAM: 2~4 / 12GB VRAM: 1~2 / 8GB VRAM: 1 권장 # 높이면 처리량↑ 하지만 각 요청에 할당되는 VRAM↓ - OLLAMA_NUM_PARALLEL=${OLLAMA_NUM_PARALLEL:-2} # OLLAMA_MAX_LOADED_MODELS: 동시에 VRAM에 올려둘 모델 수 # 여러 모델을 번갈아 쓸 때 재로딩 없이 빠른 전환 가능 # VRAM이 부족하면 1로 설정 (모델 교체 시 언로드 후 재로드) - OLLAMA_MAX_LOADED_MODELS=${OLLAMA_MAX_LOADED_MODELS:-2} # OLLAMA_FLASH_ATTENTION: Flash Attention 2 활성화 # Ampere(RTX 30xx) 이상 GPU에서 VRAM 30% 절약 + 속도↑ # Pascal(GTX 10xx) 이하에서는 0으로 설정 - OLLAMA_FLASH_ATTENTION=${OLLAMA_FLASH_ATTENTION:-1} # OLLAMA_KEEP_ALIVE: 마지막 요청 후 모델을 VRAM에 유지하는 시간 # 0: 즉시 언로드 (VRAM 절약), -1: 영구 유지, 30m: 30분 후 언로드 - OLLAMA_KEEP_ALIVE=${OLLAMA_KEEP_ALIVE:-30m} - OLLAMA_ORIGINS=* # CORS 허용 — Open WebUI 등 다른 컨테이너에서 접근 가능 - OLLAMA_MAX_QUEUE=${OLLAMA_MAX_QUEUE:-512} # 대기열 최대 크기 healthcheck: # 30초마다 API를 확인해 서비스 정상 여부를 Docker에 알림 # Open WebUI가 depends_on에서 이 healthcheck을 기다림 test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"] interval: 30s timeout: 10s retries: 5 start_period: 30s # 첫 번째 체크 전 대기 시간 (모델 초기화 시간 고려) networks: - ai_internal # 내부 서비스 통신용 (다른 컨테이너에서 ai-ollama:11434로 접근) #──────────────────────────────────────────────────────────────────────── # OPEN WEBUI — AI 통합 인터페이스 (ChatGPT 완전 대체) # ───────────────────────────────────────── # 역할: 사용자가 AI와 상호작용하는 메인 웹 인터페이스 # 왜 쓰는가: # - Ollama/OpenAI/Claude를 한 UI에서 선택 사용 # - RAG Knowledge Base 관리, 이미지 생성, 음성 입출력 통합 # - 멀티유저 권한 관리, LDAP/OAuth 연동, 사용량 추적 # - 모바일 앱에서도 동일한 UI로 접근 가능 # 제외 조건: API 전용 서버이거나 Dify로 완전 대체 시 # 연동: → ollama(LLM), → qdrant(RAG), → postgres(DB), # → searxng(웹검색), → whisper(STT), → kokoro(TTS), # → comfyui(이미지), → pipelines(필터) #──────────────────────────────────────────────────────────────────────── open-webui: image: ghcr.io/open-webui/open-webui:main container_name: ai-webui profiles: [core] <<: [*restart, *logging] ports: - "3000:8080" # 호스트 3000 → 컨테이너 8080. 브라우저에서 http://서버IP:3000 volumes: # webui_data: Open WebUI 설정, 업로드 파일, RAG 문서 인덱스 저장 # → 이 볼륨에 모든 사용자 데이터가 있음. 백업 최우선순위 1위 - webui_data:/app/backend/data environment: ## ── Ollama 연결 ───────────────────────────────────────────────────── # 컨테이너 내부에서 Ollama에 접근하는 URL # "ai-ollama"는 Docker 네트워크의 서비스명 (IP 대신 이름으로 통신) # ⚠️ localhost:11434 금지 — 컨테이너 내부에서 localhost는 자기 자신 - OLLAMA_BASE_URL=http://ai-ollama:11434 ## ── 보안 ──────────────────────────────────────────────────────────── # JWT 서명에 사용하는 비밀키 — 반드시 32자 이상 랜덤 문자열로 변경 # 이 값이 노출되면 토큰 위조 공격 가능. openssl rand -hex 32 로 생성 - WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY:?WEBUI_SECRET_KEY를 .env에 반드시 설정하세요} - WEBUI_NAME=${WEBUI_NAME:-My Private AI} # 브라우저 탭·로고에 표시되는 이름 - WEBUI_URL=${WEBUI_URL:-http://localhost:3000} # 공개 URL (OAuth 콜백에 사용) ## ── 사용자 가입 제어 ──────────────────────────────────────────────── # true: 누구나 가입 가능 (초기 설정 시). false: 가입 차단 # 관리자 계정 생성 후 반드시 false로 변경하고 docker compose up -d 재실행 - ENABLE_SIGNUP=${ENABLE_SIGNUP:-true} # 신규 가입자 기본 역할: pending(승인 대기), user(즉시 사용 가능), admin # 외부 공개 서버라면 pending 설정 필수 (무단 접근 방지) - DEFAULT_USER_ROLE=${DEFAULT_USER_ROLE:-pending} - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-86400} # 로그인 토큰 유효기간 (초). 86400=24시간 ## ── 데이터베이스 ──────────────────────────────────────────────────── # PostgreSQL을 사용하면 다중 사용자·팀 운영에 안정적 # 미설정 시 SQLite로 자동 폴백 (소규모·개인 사용 가능) - DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@ai-postgres:5432/openwebui ## ── RAG (문서 기반 AI 검색) ───────────────────────────────────────── # VECTOR_DB: 벡터 저장소 선택 (qdrant 권장. chroma는 단일노드만) - VECTOR_DB=${VECTOR_DB:-qdrant} # Qdrant 접속 정보 — ai-qdrant는 컨테이너 이름 (Docker 내부 DNS) - QDRANT_URI=http://ai-qdrant:6333 - QDRANT_API_KEY=${QDRANT_API_KEY} # 임베딩 모델: 문서를 벡터로 변환하는 모델 # BAAI/bge-m3: 한국어 포함 다국어 최고 성능 (1024차원, ~1.3GB) # nomic-embed-text: Ollama 내장 경량 모델 (768차원, ~270MB, 빠름) - RAG_EMBEDDING_MODEL=${RAG_EMBEDDING_MODEL:-BAAI/bge-m3} - RAG_EMBEDDING_ENGINE=ollama # ollama(로컬) 또는 openai(클라우드) - RAG_OLLAMA_BASE_URL=http://ai-ollama:11434 # 임베딩에 Ollama 사용 # 청킹 설정: 문서를 분할하는 단위 # CHUNK_SIZE: 하나의 청크 크기 (토큰 수). 작을수록 정밀, 클수록 맥락 풍부 - CHUNK_SIZE=${CHUNK_SIZE:-1000} # CHUNK_OVERLAP: 인접 청크 간 겹치는 크기. 0이면 문장이 잘릴 수 있음 - CHUNK_OVERLAP=${CHUNK_OVERLAP:-100} # RAG_TOP_K: 검색 시 반환할 청크 수. 많을수록 맥락↑ 하지만 프롬프트↑ - RAG_TOP_K=${RAG_TOP_K:-5} - RELEVANCE_THRESHOLD=${RELEVANCE_THRESHOLD:-0.35} # 이 점수 이하 청크 제외 # ENABLE_RAG_HYBRID_SEARCH: Dense(의미)+Sparse(키워드) 하이브리드 검색 # true: 검색 품질 20~40% 향상 (Qdrant 전용 기능) - ENABLE_RAG_HYBRID_SEARCH=${ENABLE_RAG_HYBRID_SEARCH:-true} ## ── 웹 검색 ───────────────────────────────────────────────────────── - ENABLE_RAG_WEB_SEARCH=true - RAG_WEB_SEARCH_ENGINE=searxng # <query>는 Open WebUI가 자동으로 실제 검색어로 교체 - SEARXNG_QUERY_URL=http://ai-searxng:8080/search?q=&format=json ## ── 이미지 생성 연동 ──────────────────────────────────────────────── - IMAGE_GENERATION_ENGINE=comfyui # comfyui 또는 automatic1111 - COMFYUI_BASE_URL=http://ai-comfyui:8188 ## ── 음성 AI 연동 ──────────────────────────────────────────────────── # STT: 마이크 → 텍스트 변환 (Whisper 서버 연결) - AUDIO_STT_ENGINE=openai # OpenAI 호환 API 형식 사용 - AUDIO_STT_OPENAI_API_BASE_URL=http://ai-whisper:8000/v1 - AUDIO_STT_OPENAI_API_KEY=any # Whisper는 인증 없음, 아무 값이나 가능 - AUDIO_STT_MODEL=${WHISPER_MODEL:-large-v3-turbo} # TTS: 텍스트 → 음성 변환 (Kokoro 서버 연결) - AUDIO_TTS_ENGINE=openai - AUDIO_TTS_OPENAI_API_BASE_URL=http://ai-kokoro:8880/v1 - AUDIO_TTS_OPENAI_API_KEY=any - AUDIO_TTS_MODEL=kokoro - AUDIO_TTS_VOICE=${KOKORO_DEFAULT_VOICE:-af_sarah} # 기본 성우 선택 ## ── Pipelines 연동 ───────────────────────────────────────────────── # OpenAI API Base URL을 Pipelines 서버로 설정 → 메시지 전처리 활성화 - ENABLE_OPENAI_API=true - OPENAI_API_BASE_URL=http://ai-pipelines:9099 - OPENAI_API_KEY=${PIPELINE_API_KEY} ## ── 외부 클라우드 API (폴백용) ───────────────────────────────────── # 로컬 Ollama 모델이 부족할 때 클라우드 API를 보조로 사용 가능 # 키가 없으면 비워두어도 됨 (로컬 전용 운영 가능) - OPENAI_API_KEYS=${OPENAI_API_KEY:-} depends_on: ollama: condition: service_healthy # Ollama가 완전히 준비된 후 Open WebUI 시작 ai-postgres: condition: service_healthy ai-qdrant: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 5 start_period: 60s # DB 연결 초기화 시간 고려해 60초 대기 networks: - ai_internal - ai_external # Nginx PM이 외부 트래픽을 라우팅하기 위해 같은 네트워크 필요 #──────────────────────────────────────────────────────────────────────── # PIPELINES — 메시지 전처리·후처리 미들웨어 # ───────────────────────────────────────── # 역할: 채팅 메시지를 LLM 전달 전후에 Python 코드로 처리 # 활용: 한국어 자동 감지, PII 마스킹, 콘텐츠 필터, 모델 라우팅 # 제외 조건: 메시지 전처리가 전혀 필요 없을 때 profiles에서 core 제거 #──────────────────────────────────────────────────────────────────────── ai-pipelines: image: ghcr.io/open-webui/pipelines:main container_name: ai-pipelines profiles: [core] <<: [*restart, *logging] ports: - "9099:9099" # 내부망만 — Open WebUI가 이 포트로 파이프라인 요청 전달 volumes: # pipelines_data: Python 파이프라인 파일(.py)이 저장되는 위치 # Admin Panel → Pipelines에서 업로드한 파일이 여기 저장됨 - pipelines_data:/app/pipelines environment: # Open WebUI에서 Pipelines 서버에 접속할 때 사용하는 인증 키 # .env의 PIPELINE_API_KEY와 Open WebUI 설정의 값이 일치해야 함 - PIPELINES_API_KEY=${PIPELINE_API_KEY} networks: - ai_internal #──────────────────────────────────────────────────────────────────────── # SEARXNG — 프라이버시 보호 웹 검색 엔진 # ───────────────────────────────────────── # 역할: AI가 실시간 인터넷 정보를 검색할 때 사용하는 백엔드 # 왜 쓰는가: Google/Bing API 키 없이 무료 웹 검색. 사용자 IP 보호 # 제외 조건: 웹 검색 기능이 불필요하거나 외부 검색 API 사용 시 #──────────────────────────────────────────────────────────────────────── ai-searxng: image: searxng/searxng:latest container_name: ai-searxng profiles: [core] <<: [*restart, *logging] # 포트 외부 노출 없음 — Open WebUI만 내부적으로 사용 volumes: # searxng_data: SearXNG 설정 파일(settings.yml) 저장 # 사용할 검색엔진(Google, Bing, DuckDuckGo 등) 커스텀 설정 가능 - searxng_data:/etc/searxng environment: - SEARXNG_BASE_URL=http://ai-searxng:8080 - SEARXNG_SECRET_KEY=${WEBUI_SECRET_KEY} # CSRF 보호용 비밀키 networks: - ai_internal ##═══════════════════════════════════════════════════════════════════════════ ## 🗄️ DATABASE 프로파일 — 데이터 레이어 ## core 프로파일과 함께 항상 실행하는 것이 권장입니다. ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # QDRANT — 벡터 데이터베이스 (RAG의 핵심) # ───────────────────────────────────────── # 역할: 문서를 임베딩 벡터로 저장하고 의미 기반 유사도 검색 제공 # 왜 쓰는가: # - SQL DB는 키워드 매칭, Qdrant는 의미(Semantics) 검색 # - "Docker 네트워크 설정"을 물어도 "컨테이너 통신 방법" 문서를 찾음 # - HNSW 인덱스로 수백만 건도 밀리초 검색 # - Hybrid 검색(의미+키워드 결합)으로 정확도 20~40% 향상 # 제외 조건: RAG 기능 완전 불필요 시 (ChromaDB로 대체 가능) #──────────────────────────────────────────────────────────────────────── ai-qdrant: image: qdrant/qdrant:latest container_name: ai-qdrant profiles: [core, database] # core 프로파일에서도 자동 포함 <<: [*restart, *logging] ports: - "6333:6333" # REST API — Tailscale/내부망에서 직접 접근 허용 가능 - "6334:6334" # gRPC — 대량 데이터 삽입 시 REST보다 2~3배 빠름 volumes: # qdrant_data: 모든 벡터와 페이로드(메타데이터) 저장 # → 이 볼륨을 삭제하면 Knowledge Base 전체가 사라짐! # → 백업 최우선순위: ollama_data와 함께 1순위 - qdrant_data:/qdrant/storage environment: # API Key 인증 활성화 — 키 없이는 컬렉션에 접근 불가 - QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:?QDRANT_API_KEY를 .env에 설정하세요} - QDRANT__SERVICE__ENABLE_TLS=false # Nginx/Cloudflare가 TLS 담당하므로 false - QDRANT__LOG_LEVEL=INFO - QDRANT__TELEMETRY_DISABLED=true # 원격 측정 비활성화 (프라이버시) healthcheck: test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"] interval: 30s timeout: 10s retries: 3 networks: - ai_internal #──────────────────────────────────────────────────────────────────────── # POSTGRESQL — 관계형 DB (n8n·Open WebUI·Dify 공유) # ───────────────────────────────────────── # 역할: 워크플로우·사용자·설정 등 구조화된 메타데이터 저장 # 왜 쓰는가: # - 한 인스턴스로 여러 서비스 DB를 관리 (자원 절약) # - init.sql로 n8n/openwebui/dify DB를 자동 생성 # - pgvector 확장으로 벡터 검색도 지원 (Qdrant 보조용) # 제외 조건: n8n·Dify 미사용 + Open WebUI SQLite 모드일 때 #──────────────────────────────────────────────────────────────────────── ai-postgres: image: pgvector/pgvector:pg16 # pgvector 확장 포함 공식 이미지 container_name: ai-postgres profiles: [core, database] <<: [*restart, *logging] ports: # 5432 외부 노출 금지 — 내부 서비스만 접근 가능 # Tailscale VPN으로 접근 시 UFW에서 tailscale0 인터페이스만 허용 - "127.0.0.1:5432:5432" # loopback만 바인딩 (호스트 직접 접근 시) volumes: # postgres_data: PostgreSQL 데이터 파일 (WAL 포함) # → 손상 시 n8n 워크플로우·대화 기록 전체 손실 가능. 백업 필수 - postgres_data:/var/lib/postgresql/data # init.sql: 컨테이너 최초 실행 시 자동으로 여러 DB를 생성하는 스크립트 # → n8n/openwebui/dify DB를 각각 자동 생성 + pgvector 확장 활성화 - ./configs/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro environment: # POSTGRES_USER: 슈퍼유저 계정 이름 - POSTGRES_USER=${POSTGRES_USER:-aiserver} # POSTGRES_PASSWORD: 반드시 강력한 비밀번호로 변경! # .env 파일에서 설정. 설정 안 하면 컨테이너 시작 실패 - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:?POSTGRES_PASSWORD를 .env에 설정하세요} # POSTGRES_DB: 기본 DB명 (추가 DB는 init.sql에서 생성) - POSTGRES_DB=${POSTGRES_DB:-aiserver_main} healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-aiserver}"] interval: 10s timeout: 5s retries: 5 networks: - ai_internal #──────────────────────────────────────────────────────────────────────── # REDIS — 인메모리 캐시 & 메시지 큐 # ───────────────────────────────────────── # 역할: Dify의 Celery 비동기 태스크 처리 큐 # 왜 쓰는가: # - 임베딩 처리·파이프라인 실행을 백그라운드에서 비동기 처리 # - 매우 낮은 자원 사용 (~50MB RAM) # 제외 조건: Dify를 사용하지 않을 경우 제거 가능 #──────────────────────────────────────────────────────────────────────── ai-redis: image: redis:7-alpine container_name: ai-redis profiles: [database] <<: [*restart, *logging] command: > redis-server --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD를 .env에 설정하세요} --maxmemory 2gb # 최대 메모리 제한. AI 서버 RAM에 맞게 조정 --maxmemory-policy allkeys-lru # 메모리 초과 시 오래된 캐시부터 삭제 --save 900 1 # 900초 내 1번 이상 변경 시 디스크 저장 (AOF 대안) volumes: # redis_data: Redis 스냅샷 파일 저장 (컨테이너 재시작 시 데이터 복원) - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 30s timeout: 10s retries: 3 networks: - ai_internal ##═══════════════════════════════════════════════════════════════════════════ ## 🛠️ MANAGE 프로파일 — 서버 관리 도구 ## 운영 효율성을 높이는 도구 모음. 핵심 서비스와 독립적으로 실행 가능. ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # PORTAINER CE — Docker 관리 GUI (강력 권장!) # ───────────────────────────────────────── # 역할: 브라우저에서 모든 Docker 컨테이너·볼륨·이미지를 GUI로 관리 # 왜 쓰는가: # - "docker ps, docker logs, docker exec" 명령을 클릭으로 대체 # - 팀원이 터미널 없이 컨테이너 상태·로그·재시작 가능 # - Compose 스택을 시각적으로 관리 # - 볼륨 용량 확인, 이미지 정리, 네트워크 확인 모두 UI에서 # - 무료(CE) 버전으로 홈랩·팀 운영 충분 # 제외 조건: CLI만으로 관리할 경우 제거 가능 (개인 고급 사용자) # 접속: http://서버IP:9000 → 최초 접속 시 admin 계정 생성 #──────────────────────────────────────────────────────────────────────── ai-portainer: image: portainer/portainer-ce:latest container_name: ai-portainer profiles: [manage] <<: [*restart, *logging] ports: - "9000:9000" # HTTP 관리 UI — Nginx로 HTTPS 래핑 권장 - "9443:9443" # HTTPS 관리 UI (자체 서명 인증서 포함) volumes: # Docker 소켓 — Portainer가 Docker 데몬과 통신하는 필수 연결 # ⚠️ 보안 주의: /var/run/docker.sock 마운트는 컨테이너에 Docker 전체 권한을 줌 # 내부망 전용으로만 사용하고 외부 공개 시 강력한 인증 필수 - /var/run/docker.sock:/var/run/docker.sock # portainer_data: Portainer 설정·사용자·환경 정보 저장 - portainer_data:/data networks: - ai_internal - ai_external #──────────────────────────────────────────────────────────────────────── # NGINX PROXY MANAGER — GUI 기반 리버스 프록시 & SSL 관리 # ───────────────────────────────────────── # 역할: 여러 서비스를 서브도메인으로 안전하게 외부 공개 # 왜 쓰는가: # - nginx.conf 파일 편집 없이 GUI에서 프록시 규칙 설정 # - Let's Encrypt SSL 자동 발급·갱신 (클릭 한 번) # - ai.domain.com → :3000, n8n.domain.com → :5678 라우팅 # - 접근 제어·기본 인증 레이어 추가 가능 # 제외 조건: Cloudflare Tunnel 사용 시 Nginx PM 없이도 HTTPS 가능 # 접속: http://서버IP:81 → [email protected] / changeme (최초) #──────────────────────────────────────────────────────────────────────── ai-nginx-pm: image: jc21/nginx-proxy-manager:latest container_name: ai-nginx-pm profiles: [manage] <<: [*restart, *logging] ports: - "80:80" # HTTP → HTTPS 자동 리다이렉트 - "443:443" # HTTPS (SSL 종료) - "81:81" # 관리 UI — 내부망만 허용 권장 (UFW로 외부 차단) volumes: # nginx_pm_data: 프록시 설정, SSL 인증서, 사용자 정보 저장 - nginx_pm_data:/data - nginx_pm_letsencrypt:/etc/letsencrypt # Let's Encrypt 인증서 저장 networks: - ai_internal - ai_external #──────────────────────────────────────────────────────────────────────── # WATCHTOWER — Docker 이미지 자동 업데이트 데몬 # ───────────────────────────────────────── # 역할: 지정한 주기마다 새 이미지를 확인하고 자동으로 컨테이너를 업데이트 # 왜 쓰는가: # - Ollama, Open WebUI 등 AI 도구는 주 1~2회 업데이트 # - 수동으로 docker compose pull && up -d 를 매번 실행할 필요 없음 # - 업데이트 후 Slack/이메일 알림 발송 가능 # 제외 조건: 버전을 수동으로 고정 관리할 때. 또는 프로덕션에서 # 자동 업데이트가 위험한 경우 (WATCHTOWER_LABEL_ENABLE로 선택 제어) #──────────────────────────────────────────────────────────────────────── ai-watchtower: image: containrrr/watchtower:latest container_name: ai-watchtower profiles: [manage] <<: [*restart, *logging] volumes: - /var/run/docker.sock:/var/run/docker.sock # Docker 데몬 접근 필요 environment: # 업데이트 주기: 매일 새벽 3시 (cron 형식) - WATCHTOWER_SCHEDULE=0 0 3 * * * - WATCHTOWER_CLEANUP=true # 이전 이미지 자동 삭제 (디스크 절약) - WATCHTOWER_INCLUDE_STOPPED=false # 중지된 컨테이너는 제외 # 업데이트 완료 시 Slack 알림 (선택) # - WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=https://hooks.slack.com/... # 특정 컨테이너 제외: 컨테이너에 라벨 com.centurylinklabs.watchtower.enable=false 추가 #──────────────────────────────────────────────────────────────────────── # UPTIME KUMA — 서비스 가동 상태 모니터링 & 알림 # ───────────────────────────────────────── # 역할: 각 AI 서비스 URL을 주기적으로 ping해 다운타임을 즉시 감지·알림 # 왜 쓰는가: # - Ollama가 크래시되거나 Open WebUI가 응답 없을 때 즉시 슬랙 알림 # - 예쁜 Status Page로 팀원에게 서비스 상태 공유 가능 # - HTTP·TCP·DNS·도커 컨테이너 등 다양한 모니터링 유형 지원 # 제외 조건: Grafana Alerting으로 대체하거나 알림이 불필요할 때 # 접속: http://서버IP:3001 → 최초 계정 생성 후 모니터 추가 #──────────────────────────────────────────────────────────────────────── ai-uptime: image: louislam/uptime-kuma:latest container_name: ai-uptime profiles: [manage] <<: [*restart, *logging] ports: - "3001:3001" volumes: # uptime_data: 모니터 설정, 히스토리, 알림 설정 저장 - uptime_data:/app/data networks: - ai_internal - ai_external ##═══════════════════════════════════════════════════════════════════════════ ## 🤖 AUTOMATION 프로파일 — 자동화 & AI 앱 빌더 ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # N8N — 노코드 AI 워크플로우 자동화 플랫폼 # ───────────────────────────────────────── # 역할: 이메일·슬랙·GitHub 등 500개+ 서비스를 AI와 자동 연결 # 왜 쓰는가: # - 이메일 수신 → Ollama 요약 → 슬랙 전달 (코드 없이 드래그 앤 드롭) # - 블로그 RSS 변경 → AI 번역·요약 → 자동 게시 # - GitHub PR 생성 → AI 코드 리뷰 → 자동 코멘트 # 제외 조건: 자동화가 전혀 불필요하거나 Make/Zapier 사용 시 #──────────────────────────────────────────────────────────────────────── ai-n8n: image: n8nio/n8n:latest container_name: ai-n8n profiles: [automation] <<: [*restart, *logging] ports: - "5678:5678" # 웹 에디터 & 웹훅 수신 포트 volumes: # n8n_data: 모든 워크플로우·인증정보·실행 기록 저장 # → 백업 중요도 높음. 워크플로우 재설계 비용이 크기 때문 - n8n_data:/home/node/.n8n # 호스트 데이터 폴더를 워크플로우에서 직접 접근 가능하게 마운트 - ${DATA_ROOT:-/home/ubuntu/ai-server/data}/outputs:/outputs environment: - N8N_HOST=${N8N_HOST:-localhost} - N8N_PORT=5678 - N8N_PROTOCOL=${N8N_PROTOCOL:-http} # 웹훅 URL: 외부 서비스(GitHub, Slack 등)가 n8n에 이벤트를 보낼 주소 # Nginx/Cloudflare로 외부 공개 후 공개 URL로 설정 - WEBHOOK_URL=${N8N_WEBHOOK_URL:-http://localhost:5678} # 기본 인증 (Nginx 레벨 인증과 이중 보호) - N8N_BASIC_AUTH_ACTIVE=true - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin} - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:?n8n 비밀번호 설정 필요} # 암호화 키: 인증정보(API 키, 비밀번호 등)를 암호화하는 키 # 변경하면 기존 인증정보가 복호화 불가. 처음 설정 후 절대 변경 금지 - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY:?n8n 암호화 키 설정 필요} - GENERIC_TIMEZONE=Asia/Seoul - N8N_DIAGNOSTICS_ENABLED=false # 원격 사용 데이터 전송 비활성화 # PostgreSQL 사용 (SQLite 대비 동시성·안정성 우수) - DB_TYPE=postgresdb - DB_POSTGRESDB_HOST=ai-postgres - DB_POSTGRESDB_PORT=5432 - DB_POSTGRESDB_DATABASE=n8n - DB_POSTGRESDB_USER=${POSTGRES_USER:-aiserver} - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD} depends_on: ai-postgres: condition: service_healthy networks: - ai_internal - ai_external #──────────────────────────────────────────────────────────────────────── # DIFY — AI 앱 빌더 & RAG 플랫폼 # ───────────────────────────────────────── # 역할: 복잡한 AI 앱을 노코드로 구성하고 REST API로 즉시 배포 # 왜 쓰는가: # - 다단계 RAG, 조건부 분기, 멀티에이전트를 시각적으로 설계 # - 완성된 AI 앱을 API 또는 임베드 UI로 즉시 배포 # - Open WebUI보다 복잡한 비즈니스 로직의 AI 앱에 적합 # 제외 조건: Open WebUI로 충분하거나, 커스텀 코드 개발 선호 시 # 의존성: postgres + redis 필수 (database 프로파일과 함께 실행) #──────────────────────────────────────────────────────────────────────── ai-dify: image: langgenius/dify-api:latest container_name: ai-dify profiles: [automation] <<: [*restart, *logging] ports: - "3002:5001" # Dify API 서버 포트 volumes: # dify_data: 업로드 파일, 앱 설정, 로그 저장 - dify_data:/app/api/storage environment: - SECRET_KEY=${DIFY_SECRET_KEY:?Dify 비밀키 설정 필요} # PostgreSQL 연결 - DB_USERNAME=${POSTGRES_USER:-aiserver} - DB_PASSWORD=${POSTGRES_PASSWORD} - DB_HOST=ai-postgres - DB_PORT=5432 - DB_DATABASE=dify # Redis 연결 (태스크 큐용) - REDIS_HOST=ai-redis - REDIS_PORT=6379 - REDIS_PASSWORD=${REDIS_PASSWORD} - CELERY_BROKER_URL=redis://:${REDIS_PASSWORD}@ai-redis:6379/1 # Qdrant 연결 (벡터 검색) - VECTOR_STORE=qdrant - QDRANT_URL=http://ai-qdrant:6333 - QDRANT_API_KEY=${QDRANT_API_KEY} # Ollama 연결 - OLLAMA_API_BASE_URL=http://ai-ollama:11434 depends_on: - ai-postgres - ai-redis - ai-qdrant networks: - ai_internal ##═══════════════════════════════════════════════════════════════════════════ ## 🎨 IMAGING 프로파일 — AI 이미지 생성 (GPU 집중) ## ⚠️ 주의: Ollama와 GPU를 공유합니다. 동시 실행 시 VRAM 합산 필요 ## 권장: 멀티 GPU 서버에서 GPU 번호를 분리하거나 필요할 때만 시작 ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # COMFYUI — 노드 그래프 이미지 생성 (FLUX.1 최고 지원) # ───────────────────────────────────────── # 역할: FLUX.1·SDXL·SD3 등 최신 모델로 무제한 AI 이미지 생성 # 왜 쓰는가: # - DALL-E($0.04~$0.12/장) 없이 무제한 무료 생성 # - FLUX.1 Dev 최초 지원, REST API로 n8n 자동화 가능 # - 워크플로우를 JSON으로 저장·공유·재현 가능 # 제외 조건: 이미지 생성 불필요 또는 VRAM 부족 시 # GPU: gpu-1 (멀티 GPU 서버에서 Ollama와 분리 권장) #──────────────────────────────────────────────────────────────────────── ai-comfyui: image: yanwk/comfyui-boot:cu124 container_name: ai-comfyui profiles: [imaging] <<: [*gpu-1, *restart, *logging] # 싱글 GPU면 *gpu-all로 교체 ports: - "8188:8188" volumes: # comfyui_models: Checkpoint·LoRA·VAE·ControlNet 등 모델 파일 # → Ollama와 모델 폴더 공유로 스토리지 절약 (shared_models) - comfyui_models:/root/ComfyUI/models # comfyui_output: 생성된 이미지 저장. 호스트에서 직접 접근 가능 - comfyui_output:/root/ComfyUI/output # comfyui_nodes: ComfyUI Manager로 설치한 커스텀 노드 - comfyui_nodes:/root/ComfyUI/custom_nodes environment: - NVIDIA_VISIBLE_DEVICES=all # CLI_ARGS: ComfyUI 실행 옵션 # --listen 0.0.0.0: 외부 접근 허용 (내부망 접근 위해 필요) # --preview-method auto: 생성 중 미리보기 활성화 - CLI_ARGS=${COMFYUI_ARGS:---listen 0.0.0.0 --port 8188 --preview-method auto} shm_size: 8gb # 공유 메모리. 배치 이미지 생성 시 부족하면 OOM 발생 networks: - ai_internal #──────────────────────────────────────────────────────────────────────── # STABLE DIFFUSION A1111 — 직관적 UI 이미지 생성 # ───────────────────────────────────────── # 역할: 탭 방식 UI로 빠른 이미지 생성 실험. Extension 생태계 풍부 # 왜 쓰는가: # - 초보자도 쉬운 UI (txt2img, img2img, Inpainting 탭) # - ADetailer·ControlNet·LoRA 등 수백 개 Extension # - ComfyUI와 역할 분리: 실험은 A1111, 자동화는 ComfyUI # 제외 조건: ComfyUI만 사용하거나 이미지 생성 불필요 시 #──────────────────────────────────────────────────────────────────────── ai-a1111: image: universalml/stable-diffusion-webui:latest container_name: ai-a1111 profiles: [imaging] <<: [*gpu-1, *restart, *logging] # 싱글 GPU면 *gpu-all로 교체 ports: - "7860:7860" volumes: - a1111_models:/stable-diffusion-webui/models - a1111_output:/stable-diffusion-webui/outputs - a1111_extensions:/stable-diffusion-webui/extensions environment: - NVIDIA_VISIBLE_DEVICES=all # CLI_ARGS 핵심 옵션 설명: # --listen: 외부 접근 허용 # --api: REST API 활성화 (n8n 연동 필수) # --xformers: VRAM 20~30% 절약 (RTX 2080 이상 권장) # --medvram: 12GB 이하 VRAM에서 OOM 방지 - CLI_ARGS=${A1111_ARGS:---listen --api --xformers --no-half-vae} networks: - ai_internal ##═══════════════════════════════════════════════════════════════════════════ ## 🎙️ VOICE 프로파일 — 음성 AI 서비스 ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # FASTER-WHISPER — 고성능 음성 인식 서버 (STT) # ───────────────────────────────────────── # 역할: 마이크 음성을 텍스트로 변환. OpenAI Whisper API 형식 호환 # 왜 쓰는가: # - OpenAI Whisper API($0.006/분) 없이 무제한 무료 STT # - original Whisper보다 4배 빠르고 VRAM 절반만 사용 # - 한국어 포함 99개 언어 지원 # - 기존 OpenAI Whisper API 코드를 URL만 변경으로 교체 가능 # 제외 조건: 음성 입력 기능이 불필요할 때 #──────────────────────────────────────────────────────────────────────── ai-whisper: image: fedirz/faster-whisper-server:latest-cuda container_name: ai-whisper profiles: [voice] <<: [*gpu-all, *restart, *logging] ports: - "8000:8000" # OpenAI /v1/audio/transcriptions 호환 API volumes: # whisper_models: 다운로드한 Whisper 모델 캐시 # first 실행 시 모델 자동 다운로드 (~3GB for large-v3-turbo) - whisper_models:/root/.cache/huggingface environment: # 모델 선택: tiny/base/small/medium/large-v3/large-v3-turbo # large-v3-turbo: large-v3와 동일 품질, 처리속도 2배 (권장) - WHISPER__MODEL=${WHISPER_MODEL:-large-v3-turbo} - WHISPER__INFERENCE_DEVICE=cuda # GPU 사용. CPU만 있으면 cpu로 변경 - WHISPER__COMPUTE_TYPE=float16 # FP16으로 VRAM 절반 사용. int8은 더 빠름 # 언어 고정: ko로 설정하면 한국어 인식 정확도↑. 다국어면 비워둠 - WHISPER__LANGUAGE=${WHISPER_LANGUAGE:-ko} - WHISPER__VAD_FILTER=true # 무음 구간 자동 감지·제거 (정확도↑) networks: - ai_internal #──────────────────────────────────────────────────────────────────────── # KOKORO TTS — 고품질 음성 합성 서버 # ───────────────────────────────────────── # 역할: AI 텍스트 답변을 자연스러운 음성으로 변환 (TTS) # 왜 쓰는가: # - ElevenLabs($22/월) 없이 무제한 무료 TTS # - 82M 파라미터 초경량이지만 ElevenLabs 수준의 자연스러운 음성 # - CPU만으로도 실시간 스트리밍 가능 (GPU 불필요) # - OpenAI TTS API 형식 호환 (api.openai.com 주소만 변경) # 제외 조건: 음성 출력이 불필요하거나 텍스트만 보는 경우 #──────────────────────────────────────────────────────────────────────── ai-kokoro: image: ghcr.io/remsky/kokoro-fastapi-cpu:latest # CPU 버전 (GPU 버전도 있음) container_name: ai-kokoro profiles: [voice] <<: [*restart, *logging] ports: - "8880:8880" # OpenAI /v1/audio/speech 호환 API volumes: - kokoro_models:/app/models # Kokoro 음성 모델 캐시 environment: # 기본 성우 선택: af_sarah(여성·부드러움), af_bella(여성·활기참), # am_adam(남성·차분함), am_michael(남성·전문적) - KOKORO_DEFAULT_VOICE=${KOKORO_DEFAULT_VOICE:-af_sarah} networks: - ai_internal ##═══════════════════════════════════════════════════════════════════════════ ## 📊 MONITORING 프로파일 — 관측 가능성 스택 ##═══════════════════════════════════════════════════════════════════════════ #──────────────────────────────────────────────────────────────────────── # PROMETHEUS — 메트릭 수집 & 시계열 DB # 역할: GPU·CPU·RAM·컨테이너의 모든 수치를 시계열로 저장 # 제외 조건: Grafana 없이 Uptime Kuma만으로 모니터링할 경우 #──────────────────────────────────────────────────────────────────────── ai-prometheus: image: prom/prometheus:latest container_name: ai-prometheus profiles: [monitoring] <<: [*restart, *logging] ports: - "9090:9090" volumes: - prometheus_data:/prometheus # 수집된 메트릭 데이터 저장 (30일 보존) # prometheus.yml: 어떤 서비스에서 메트릭을 수집할지 설정 - ./configs/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - ./configs/prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml:ro command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.retention.time=30d' # 30일 이전 데이터 자동 삭제 - '--web.enable-lifecycle' # API로 설정 리로드 가능 networks: - ai_internal #──────────────────────────────────────────────────────────────────────── # GRAFANA — 메트릭 시각화 & 알림 대시보드 # 역할: Prometheus 데이터를 실시간 그래프·경고로 시각화 # 접속: http://서버IP:3003 → admin / (GRAFANA_ADMIN_PASSWORD) #──────────────────────────────────────────────────────────────────────── ai-grafana: image: grafana/grafana:latest container_name: ai-grafana profiles: [monitoring] <<: [*restart, *logging] ports: - "3003:3000" # 호스트 3003 (3000은 Open WebUI가 사용 중) volumes: - grafana_data:/var/lib/grafana # 대시보드·데이터소스·알림 설정 저장 environment: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?Grafana 비밀번호 설정 필요} - GF_USERS_ALLOW_SIGN_UP=false # 외부 가입 차단 - GF_ANALYTICS_REPORTING_ENABLED=false # 원격 측정 비활성화 depends_on: - ai-prometheus networks: - ai_internal #── GPU 메트릭 수집기 ────────────────────────────────────────────────── ai-nvidia-exporter: image: utkuozdemir/nvidia_gpu_exporter:1.2.0 container_name: ai-nvidia-exporter profiles: [monitoring] <<: [*restart, *logging] ports: - "9835:9835" # Prometheus가 이 포트에서 GPU 메트릭 수집 devices: - /dev/nvidiactl # NVIDIA 제어 장치 (필수) - /dev/nvidia0 # GPU 0번. GPU가 여러 개면 nvidia1, nvidia2 추가 volumes: # NVIDIA 관리 라이브러리. 경로가 다르면 find /usr -name "libnvidia-ml.so*" - /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1 networks: - ai_internal #── 시스템 메트릭 수집기 ─────────────────────────────────────────────── ai-node-exporter: image: prom/node-exporter:latest container_name: ai-node-exporter profiles: [monitoring] <<: [*restart, *logging] ports: - "9100:9100" volumes: - /proc:/host/proc:ro # 프로세스 정보 읽기 전용 마운트 - /sys:/host/sys:ro # 시스템 정보 읽기 전용 마운트 - /:/rootfs:ro command: - '--path.procfs=/host/proc' - '--path.sysfs=/host/sys' network_mode: host # 호스트 네트워크 직접 접근 (정확한 네트워크 메트릭) #── 컨테이너 메트릭 수집기 ───────────────────────────────────────────── ai-cadvisor: image: gcr.io/cadvisor/cadvisor:latest container_name: ai-cadvisor profiles: [monitoring] <<: [*restart, *logging] ports: - "8090:8080" volumes: - /:/rootfs:ro - /var/run:/var/run:ro - /sys:/sys:ro - /var/lib/docker/:/var/lib/docker:ro networks: - ai_internal ##═══════════════════════════════════════════════════════════════════════════ ## 📦 볼륨 정의 — 모든 영속 데이터 중앙 관리 ## 이름을 명시하면 Docker가 자동으로 ai-server_ 접두사를 제거하고 ## 지정한 이름 그대로 볼륨을 생성합니다. ##═══════════════════════════════════════════════════════════════════════════ volumes: # Core ollama_data: { name: ai-ollama-data } # ★★★ AI 모델 파일 (가장 큰 볼륨) webui_data: { name: ai-webui-data } # ★★★ 사용자·대화·설정 pipelines_data: { name: ai-pipelines-data } # 파이프라인 Python 파일 searxng_data: { name: ai-searxng-data } # 검색엔진 설정 # Database qdrant_data: { name: ai-qdrant-data } # ★★★ 벡터 DB (RAG 핵심) postgres_data: { name: ai-postgres-data } # ★★★ 관계형 DB redis_data: { name: ai-redis-data } # 캐시 (재생성 가능) # Management portainer_data: { name: ai-portainer-data } # Portainer 설정 nginx_pm_data: { name: ai-nginx-pm-data } # 프록시 규칙·SSL nginx_pm_letsencrypt: { name: ai-nginx-pm-le } # Let's Encrypt 인증서 uptime_data: { name: ai-uptime-data } # 모니터 설정·히스토리 # Automation n8n_data: { name: ai-n8n-data } # ★★ 워크플로우 (복구 불가) dify_data: { name: ai-dify-data } # Dify 앱 설정 # Imaging comfyui_models: { name: ai-comfyui-models } # 이미지 생성 모델 파일 comfyui_output: { name: ai-comfyui-output } # 생성된 이미지 comfyui_nodes: { name: ai-comfyui-nodes } # 커스텀 노드 a1111_models: { name: ai-a1111-models } # A1111 모델 파일 a1111_output: { name: ai-a1111-output } # A1111 생성 이미지 a1111_extensions: { name: ai-a1111-ext } # A1111 Extensions # Voice whisper_models: { name: ai-whisper-models } # Whisper 모델 캐시 kokoro_models: { name: ai-kokoro-models } # Kokoro TTS 모델 # Monitoring prometheus_data: { name: ai-prometheus-data } # 30일치 메트릭 데이터 grafana_data: { name: ai-grafana-data } # 대시보드 설정 ##═══════════════════════════════════════════════════════════════════════════ ## 🌐 네트워크 정의 — 내부/외부 레이어 분리 ##═══════════════════════════════════════════════════════════════════════════ networks: # ai_internal: 서비스 간 내부 통신 전용 # 컨테이너 이름(ai-ollama, ai-postgres 등)이 DNS처럼 동작 # 이 네트워크의 포트는 외부에서 직접 접근 불가 ai_internal: name: ai-internal-net driver: bridge ipam: config: - subnet: 172.20.0.0/16 # ai_external: Nginx PM이 외부 트래픽을 받아 내부 서비스로 전달하는 경로 # open-webui, n8n, portainer 등 외부 공개가 필요한 서비스만 포함 ai_external: name: ai-external-net driver: bridge ipam: config: - subnet: 172.21.0.0/16
환경변수(.env) 완전 해설
~/ai-server/compose/.env 파일의 모든 변수를 서비스별로 완전하게 설명합니다. 필수는 반드시 변경, 선택은 기본값으로 두어도 됩니다.
| 변수명 | 필수 | 기본값 | 설명 |
|---|---|---|---|
| AI_SERVER_IP | 선택 | 192.168.1.253 | 서버 내부 IP. 스크립트에서 참조용으로 사용 |
| DATA_ROOT | 선택 | /home/ubuntu/ai-server/data | 호스트의 데이터 루트 디렉토리. 모델 파일·생성 결과물 저장 경로 |
| 변수명 | 필수 | 기본값 | 설명 |
|---|---|---|---|
| WEBUI_SECRET_KEY | 필수 | — | Open WebUI JWT 서명 키. 32자 이상 랜덤. 변경 시 모든 세션 만료 |
| QDRANT_API_KEY | 필수 | — | Qdrant 접근 인증 키. 없으면 누구나 벡터 DB에 접근 가능 |
| POSTGRES_PASSWORD | 필수 | — | PostgreSQL 슈퍼유저 비밀번호. 설정 후 절대 변경 금지 (마이그레이션 필요) |
| REDIS_PASSWORD | 필수 | — | Redis 인증 비밀번호 |
| N8N_ENCRYPTION_KEY | 필수 | — | n8n의 API Key·비밀번호 암호화 키. 변경 시 기존 인증정보 복호화 불가 |
| PIPELINE_API_KEY | 선택 | any_string | Open WebUI ↔ Pipelines 서버 인증 키 |
| DIFY_SECRET_KEY | 선택 | — | Dify 사용 시 필수. 32자 랜덤 문자열 |
| 변수명 | 필수 | 기본값 | 설명 및 권장값 |
|---|---|---|---|
| OLLAMA_NUM_PARALLEL | 선택 | 2 | 동시 요청 처리 수. 24GB VRAM: 2~4, 12GB: 1~2, 8GB: 1 |
| OLLAMA_MAX_LOADED_MODELS | 선택 | 2 | VRAM에 동시 상주할 모델 수. 모델 전환 시 재로딩 시간 단축 |
| OLLAMA_FLASH_ATTENTION | 선택 | 1 | 1: 활성화(RTX 20xx 이상). 0: 비활성화(구형 GPU). VRAM 30% 절약 |
| OLLAMA_KEEP_ALIVE | 선택 | 30m | 모델 VRAM 유지 시간. 0=즉시 해제, -1=영구 유지, 30m=30분 |
| 변수명 | 필수 | 기본값 | 설명 |
|---|---|---|---|
| WEBUI_NAME | 선택 | My Private AI | 브라우저 탭·로그인 화면에 표시되는 서비스 이름 |
| WEBUI_URL | 선택 | http://localhost:3000 | 외부 공개 URL. Google OAuth 사용 시 정확히 일치해야 함 |
| ENABLE_SIGNUP | 선택 | true | 가입 허용 여부. 관리자 계정 생성 후 반드시 false로 변경 |
| DEFAULT_USER_ROLE | 선택 | pending | 신규 가입자 기본 역할. pending(관리자 승인 필요) 권장 |
| RAG_EMBEDDING_MODEL | 선택 | BAAI/bge-m3 | RAG 임베딩 모델. 한국어: BAAI/bge-m3 권장. 경량: nomic-embed-text |
| CHUNK_SIZE | 선택 | 1000 | 문서 청킹 크기(토큰). 작으면 정밀↑ 검색 속도↑, 크면 맥락↑ |
| RAG_TOP_K | 선택 | 5 | 검색 반환 청크 수. 많을수록 맥락 풍부, 프롬프트 토큰↑ |
| ENABLE_RAG_HYBRID_SEARCH | 선택 | true | Dense+Sparse 하이브리드 검색. Qdrant 사용 시 true 권장 |
| OPENAI_API_KEY | 선택 | — | OpenAI API 키. 없으면 외부 API 사용 불가 (로컬만 사용 가능) |
| 변수명 | 필수 | 기본값 | 설명 |
|---|---|---|---|
| WHISPER_MODEL | 선택 | large-v3-turbo | tiny/base/small/medium/large-v3/large-v3-turbo. 한국어 권장: medium 이상 |
| WHISPER_LANGUAGE | 선택 | ko | 언어 고정 (한국어 정확도↑). 다국어 서버라면 제거 |
| KOKORO_DEFAULT_VOICE | 선택 | af_sarah | 기본 성우. af_sarah(여성)/am_adam(남성)/af_bella(활기)/am_michael(전문) |
① .env 파일을 Git에 절대 커밋하지 마세요 — .gitignore에 추가 필수. ② 파일 권한을 chmod 600 .env로 설정 (소유자만 읽기). ③ 모든 비밀번호는 openssl rand -hex 32로 생성한 랜덤 값 사용. ④ POSTGRES_PASSWORD, N8N_ENCRYPTION_KEY는 설정 후 절대 변경 금지.
볼륨 & 네트워크 완전 해설 — 데이터 저장 구조
📦 볼륨별 저장 내용 & 백업 중요도
🌐 네트워크 레이어 분리 설계
ai-internal-net: 모든 서비스가 포함. Qdrant, PostgreSQL 같은 DB는 이 네트워크에서만 접근 가능. 외부 인터넷에서 직접 접근 불가. 컨테이너 이름이 DNS처럼 작동해 ai-ollama:11434로 Ollama에 접근합니다. ai-external-net: Nginx PM + 외부 공개 서비스만 포함. Nginx PM이 443포트로 받은 요청을 이 네트워크를 통해 내부 서비스로 전달합니다. DB는 이 네트워크에 포함되지 않아 외부에서 절대 직접 접근 불가합니다.
서비스 기동 · Portainer 설정 · 초기 설정 가이드
cd ~/ai-server/compose ## ── 1단계: .env 설정 (실행 전 필수!) ───────────── cp .env.example .env # 예시 파일 복사 nano .env # 비밀번호·키 전부 변경 # 비밀번호 자동 생성 도우미 echo "WEBUI_SECRET_KEY=$(openssl rand -hex 32)" echo "QDRANT_API_KEY=$(openssl rand -hex 24)" echo "POSTGRES_PASSWORD=$(openssl rand -hex 16)" echo "REDIS_PASSWORD=$(openssl rand -hex 16)" echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)" ## ── 2단계: PostgreSQL 초기화 스크립트 생성 ──────── mkdir -p configs/postgres cat > configs/postgres/init.sql << 'SQL' CREATE DATABASE n8n WITH OWNER aiserver ENCODING 'UTF8' TEMPLATE template0; CREATE DATABASE openwebui WITH OWNER aiserver ENCODING 'UTF8' TEMPLATE template0; CREATE DATABASE dify WITH OWNER aiserver ENCODING 'UTF8' TEMPLATE template0; \c aiserver_main; CREATE EXTENSION IF NOT EXISTS vector; \c openwebui; CREATE EXTENSION IF NOT EXISTS vector; SQL ## ── 3단계: 핵심 서비스 시작 ─────────────────────── # 처음에는 core+database+manage만 시작 (가장 안전한 시작) docker compose \ --profile core \ --profile database \ --profile manage \ --env-file .env \ up -d ## ── 4단계: 기동 상태 모니터링 (모두 healthy 될 때까지) ─ # 약 1~3분 소요. Ctrl+C로 종료 watch -n 3 'docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | grep ai-' ## ── 5단계: AI 모델 다운로드 ──────────────────────── # 추천 기본 세트 (총 약 20GB) docker exec -it ai-ollama ollama pull qwen2.5:14b # 한국어 최강 ~9GB docker exec -it ai-ollama ollama pull nomic-embed-text # RAG 임베딩 필수 ~270MB docker exec -it ai-ollama ollama pull gemma3:12b # Vision 지원 ~8GB docker exec -it ai-ollama ollama pull deepseek-r1:8b # 수학·코딩 ~5GB # 모델 목록 및 GPU 사용 확인 docker exec -it ai-ollama ollama list docker exec -it ai-ollama ollama ps ## ── 6단계: Portainer 초기 설정 ───────────────────── # 브라우저에서 http://서버IP:9000 접속 # 1. 관리자 계정 생성 (최초 5분 내 완료해야 함) # 2. "Get Started" → "local" 환경 선택 # 3. 모든 컨테이너·볼륨·네트워크가 GUI에서 보임 echo "Portainer: http://$(hostname -I | awk '{print $1}'):9000" ## ── 7단계: Open WebUI 초기 설정 ──────────────────── # 브라우저에서 http://서버IP:3000 접속 # 1. 첫 번째 계정 생성 → 자동으로 Admin 권한 부여 # 2. .env에서 ENABLE_SIGNUP=false 로 변경 # 3. docker compose up -d 로 재시작 (변경사항 적용) docker compose --profile core --profile database --profile manage up -d ## ── 8단계: 추가 서비스 기동 ──────────────────────── # 자동화 서비스 추가 docker compose --profile automation --env-file .env up -d # 이미지 생성 (GPU VRAM 여유 있을 때) docker compose --profile imaging --env-file .env up -d # 음성 AI docker compose --profile voice --env-file .env up -d # 모니터링 docker compose --profile monitoring --env-file .env up -d ## ── 9단계: 전체 상태 확인 ────────────────────────── # 서비스 상태 docker ps --format "table {{.Names}}\t{{.Status}}" | grep ai- # 디스크 사용량 docker system df # GPU 상태 nvidia-smi
📌 Portainer 주요 기능 활용 가이드
| 작업 | Portainer 경로 | CLI 동등 명령 |
|---|---|---|
| 컨테이너 실시간 로그 확인 | Containers → ai-ollama → Logs | docker logs -f ai-ollama |
| 컨테이너 재시작 | Containers → 컨테이너 선택 → Restart | docker restart ai-webui |
| 볼륨 크기 확인 | Volumes → ai-ollama-data → 클릭 | docker system df -v |
| 컨테이너 내부 접속 | Containers → 컨테이너 → Console | docker exec -it ai-ollama bash |
| 환경변수 확인 | Containers → 컨테이너 → Inspect → Env | docker inspect ai-webui |
| 이미지 업데이트 | Images → Pull → 이미지명 입력 | docker compose pull |
| Compose 스택 전체 보기 | Stacks → ai-server | docker compose ps |
🎉 글 1 완성! 23개 컨테이너의 역할·필요성·제외 조건을 파악하고, 초주석 통합 Docker Compose로 전체 AI 스택을 단 하나의 파일로 관리합니다. Portainer로 GUI 관리까지 더해졌습니다. 글 2에서는 이 서버를 UFW·Nginx·Cloudflare Tunnel로 안전하게 외부에 공개하는 방법을 다룹니다.
양자화 완전 이론 — 정밀도 vs 품질 트레이드오프
AI 모델의 가중치는 원래 32비트 부동소수점(FP32)으로 저장됩니다. 이를 더 낮은 비트 수로 변환하는 것이 양자화(Quantization)입니다. Llama 3.3 70B 모델을 예로 들면 FP32로는 280GB, FP16으로는 140GB, INT4로는 35GB가 필요합니다. RTX 3090(24GB VRAM) 한 장으로 70B를 구동하려면 반드시 강력한 양자화가 필요합니다.
| 정밀도 | 비트 | 70B VRAM | 품질 손실 | 속도 | 사용 시나리오 |
|---|---|---|---|---|---|
| FP32 | 32bit | 280GB | 🟢 없음 (기준) | 🔴 최저 | 학습(Training)에만 사용 |
| BF16 | 16bit | 140GB | 🟢 거의 없음 | 🟡 보통 | A100/H100 학습·파인튜닝 |
| FP16 | 16bit | 140GB | 🟢 거의 없음 | 🟡 보통 | vLLM 추론 기본값 |
| INT8 (GPTQ) | 8bit | 70GB | 🟡 미미 | 🟢 빠름 | 품질 우선 양자화 |
| Q5_K_M (GGUF) | ~5bit | ~48GB | 🟡 미미 | 🟢 빠름 | 멀티 GPU 추천 |
| Q4_K_M (GGUF) | ~4bit | ~42GB | 🟡 소폭 | 🟢 빠름 | 단일 24GB VRAM 추천 |
| Q3_K_M (GGUF) | ~3bit | ~32GB | 🔴 눈에 띔 | 🟢 매우 빠름 | VRAM 극한 절약 (품질 희생) |
GGUF는 llama.cpp 포맷으로 CPU+GPU 혼합 추론이 가능합니다. VRAM이 부족할 때 일부 레이어를 RAM에서 처리합니다. GPTQ는 GPU 전용이며 INT4/INT8 정밀도로 속도가 빠릅니다. AWQ는 최신 양자화 방법으로 같은 INT4라도 GPTQ보다 품질이 더 좋습니다. 결론: GPU VRAM이 충분하면 AWQ > GPTQ, VRAM이 부족해 CPU 메모리도 활용해야 하면 GGUF를 선택하세요.
vLLM — OpenAI 호환 고성능 추론 서버
vLLM은 UC Berkeley에서 개발한 LLM 추론 엔진으로, PagedAttention이라는 혁신적인 메모리 관리 기법으로 Ollama 대비 최대 24배 높은 처리량을 달성합니다. 특히 여러 사용자가 동시에 요청할 때의 차이가 극적입니다. OpenAI API와 완전히 호환되어 기존 코드를 변경 없이 그대로 사용할 수 있습니다.
기존 LLM 추론은 각 요청마다 고정된 KV Cache 메모리를 미리 할당합니다. 요청이 10개이면 최대 길이 기준으로 10배 메모리를 낭비합니다. PagedAttention은 OS의 가상 메모리처럼 KV Cache를 페이지 단위로 동적 할당합니다. 실제 사용하는 만큼만 VRAM을 씁니다. 결과적으로 같은 VRAM에서 훨씬 많은 동시 요청을 처리하고, Continuous Batching으로 요청이 완료된 슬롯에 즉시 새 요청을 채웁니다.
| 항목 | Ollama | vLLM |
|---|---|---|
| 동시 사용자 처리 | 순차 처리 (기다림) | Continuous Batching (즉시 처리) |
| 처리량 (Throughput) | 기준 1x | 최대 24x |
| API 호환성 | Ollama 전용 + OpenAI 호환 | 완전 OpenAI 호환 |
| 모델 지원 | GGUF 중심 | HuggingFace 모든 모델 |
| LoRA 어댑터 | 기본 지원 | 동적 LoRA 로딩 지원 |
| 추천 상황 | 개인 사용, 소규모 팀 | 멀티유저, 프로덕션 서비스 |
services:
vllm:
image: vllm/vllm-openai:latest
container_name: vllm_server
restart: unless-stopped
ports:
- "8001:8000" # Ollama 8000과 충돌 방지
volumes:
- huggingface_cache:/root/.cache/huggingface
environment:
- HUGGING_FACE_HUB_TOKEN=your_hf_token # 비공개 모델 접근용
command: >
--model Qwen/Qwen2.5-14B-Instruct
--served-model-name qwen2.5-14b
--api-key your_vllm_api_key
--max-model-len 16384
--gpu-memory-utilization 0.90
--tensor-parallel-size 1 # GPU 수 (멀티 GPU 시 2, 4 등)
--enable-chunked-prefill
--enable-prefix-caching # 시스템 프롬프트 캐싱 (성능↑)
--max-num-seqs 256 # 최대 동시 시퀀스 수
--host 0.0.0.0
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
shm_size: '16gb' # PagedAttention 공유 메모리
networks:
- ai_net
volumes:
huggingface_cache:
networks:
ai_net:
external: true
name: ai_networkfrom openai import OpenAI # vLLM을 OpenAI 클라이언트로 그대로 사용! client = OpenAI( api_key="your_vllm_api_key", base_url="http://192.168.1.253:8001/v1" ) ## 스트리밍 응답 stream = client.chat.completions.create( model="qwen2.5-14b", messages=[{"role": "user", "content": "AI 서버 최적화 방법을 알려줘"}], max_tokens=500, temperature=0.7, stream=True ) for chunk in stream: if chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True) ## 배치 처리 (여러 요청 동시) import asyncio from openai import AsyncOpenAI async_client = AsyncOpenAI( api_key="your_vllm_api_key", base_url="http://192.168.1.253:8001/v1" ) async def batch_inference(prompts: list[str]) -> list[str]: """여러 프롬프트를 동시에 처리 (vLLM의 진가)""" tasks = [ async_client.chat.completions.create( model="qwen2.5-14b", messages=[{"role": "user", "content": p}], max_tokens=300, ) for p in prompts ] results = await asyncio.gather(*tasks) return [r.choices[0].message.content for r in results] # 100개 요청 동시 처리 (Ollama는 순차, vLLM은 병렬) prompts = [f"질문 {i}: AI 서버 구축 팁을 한 줄로" for i in range(100)] answers = asyncio.run(batch_inference(prompts)) print(f"✅ {len(answers)}개 응답 동시 완료")
Ollama(포트 11434)는 Open WebUI 연동·개인 채팅용, vLLM(포트 8001)은 n8n·자동화·배치 처리·API 서비스용으로 역할을 분리하면 최적의 성능을 발휘합니다. Open WebUI에서는 두 백엔드를 동시에 등록해 모델별로 선택할 수 있습니다.
GGUF 양자화 실전 — llama.cpp 서버
HuggingFace에서 내려받은 원본 모델을 직접 GGUF로 양자화하면, 공개된 양자화 버전이 없는 최신 모델도 즉시 사용할 수 있습니다. 또한 정밀도를 직접 선택해 VRAM과 품질을 최적으로 균형 잡을 수 있습니다.
# llama.cpp GPU 지원 빌드 git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build -DLLAMA_CUDA=ON -DCUDA_ARCHITECTURES=89 # RTX 4090: 89, RTX 3090: 86 cmake --build build --config Release -j$(nproc) # HuggingFace에서 원본 모델 다운로드 pip install huggingface_hub huggingface-cli download Qwen/Qwen2.5-14B-Instruct \ --local-dir ./models/qwen2.5-14b-fp16 # FP16으로 변환 (중간 단계) python convert_hf_to_gguf.py ./models/qwen2.5-14b-fp16 \ --outtype f16 \ --outfile ./models/qwen2.5-14b-f16.gguf # 다양한 양자화 수준으로 변환 ./build/bin/llama-quantize \ ./models/qwen2.5-14b-f16.gguf \ ./models/qwen2.5-14b-Q4_K_M.gguf \ Q4_K_M # 12GB VRAM: Q4_K_M 추천 ./build/bin/llama-quantize \ ./models/qwen2.5-14b-f16.gguf \ ./models/qwen2.5-14b-Q8_0.gguf \ Q8_0 # 16GB VRAM: Q8_0 고품질 # 양자화 품질 벤치마크 (perplexity로 품질 측정) ./build/bin/llama-perplexity \ -m ./models/qwen2.5-14b-Q4_K_M.gguf \ -f wikitext-2-raw/wiki.test.raw # llama.cpp 서버 실행 (OpenAI 호환) ./build/bin/llama-server \ --model ./models/qwen2.5-14b-Q4_K_M.gguf \ --host 0.0.0.0 \ --port 8002 \ --n-gpu-layers 48 \ # GPU로 올릴 레이어 수 (많을수록 빠름) --ctx-size 8192 \ --n-parallel 4 \ # 동시 처리 요청 수 --flash-attn # Flash Attention 활성화
💾 VRAM별 최적 GGUF 양자화 전략
| VRAM | 14B 모델 | 27B 모델 | 70B 모델 | n-gpu-layers |
|---|---|---|---|---|
| 8GB | Q4_K_M (전체) | Q4_K_M 일부만 | 불가 | ~35 |
| 12GB | Q8_0 (고품질) | Q4_K_M (일부) | 불가 | ~48 |
| 16GB | FP16 (원본급) | Q8_0 (전체) | Q4_K_M 일부 | 전체 |
| 24GB | FP16 + 빠름 | FP16 (전체) | Q4_K_M (대부분) | 전체 |
| 48GB (듀얼) | — | FP16 완벽 | Q8_0 (전체) | 전체 |
# Modelfile 생성 (커스텀 파라미터 포함) cat > Modelfile << 'EOF' FROM /path/to/qwen2.5-14b-Q4_K_M.gguf PARAMETER num_ctx 8192 PARAMETER num_gpu 48 PARAMETER num_thread 8 PARAMETER temperature 0.7 PARAMETER top_p 0.9 PARAMETER repeat_penalty 1.1 SYSTEM """당신은 한국어 전문 AI 어시스턴트입니다. 항상 정확하고 도움이 되는 답변을 제공합니다.""" EOF # Ollama에 등록 docker exec -it ollama ollama create qwen2.5-14b-custom -f Modelfile # 등록 확인 docker exec -it ollama ollama list docker exec -it ollama ollama run qwen2.5-14b-custom "안녕하세요"
LoRA 파인튜닝 — Unsloth로 나만의 특화 모델
LoRA(Low-Rank Adaptation)는 원본 모델 가중치를 고정하고 작은 어댑터 레이어만 학습하는 파인튜닝 방법입니다. Unsloth는 LoRA 파인튜닝을 기존 방법보다 2~5배 빠르게, VRAM은 60~70% 절약하면서 수행합니다. RTX 3090(24GB)으로 14B 모델을 2~3시간 만에 특화시킬 수 있습니다.
RAG로도 해결되지 않는 상황에서 파인튜닝을 고려하세요. 특정 도메인 용어를 기본으로 이해해야 할 때(법률, 의학, 특수 산업), 특정 출력 형식을 항상 지켜야 할 때(특정 JSON 스키마, 보고서 양식), 특정 말투·페르소나를 유지해야 할 때(브랜드 챗봇)가 대표적입니다. 단순히 최신 정보 업데이트나 일반 질답은 RAG가 더 효율적입니다.
## 설치: pip install "unsloth[colab-new] @ git+https://github.com/unslothai/unsloth.git" ## pip install --no-deps trl peft accelerate bitsandbytes from unsloth import FastLanguageModel from trl import SFTTrainer from transformers import TrainingArguments from datasets import Dataset import torch ## ── 1. 모델 로드 (4bit 양자화로 VRAM 절약) ──────── model, tokenizer = FastLanguageModel.from_pretrained( model_name = "Qwen/Qwen2.5-14B-Instruct", max_seq_length = 4096, dtype = None, # 자동 감지 (RTX: BF16) load_in_4bit = True, # 24GB VRAM으로 14B 학습 가능 ) ## ── 2. LoRA 어댑터 설정 ───────────────────────── model = FastLanguageModel.get_peft_model( model, r = 32, # LoRA 랭크. 높을수록 정교하지만 느림. 16~64 권장 target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], lora_alpha = 64, # LoRA 스케일. r * 2 권장 lora_dropout = 0.05, bias = "none", use_gradient_checkpointing = "unsloth", # VRAM 40% 추가 절약 random_state = 42, ) ## ── 3. 학습 데이터셋 준비 ────────────────────── # Alpaca 형식 데이터 (instruction, input, output) ALPACA_PROMPT = """Below is an instruction that describes a task. Write a response that appropriately completes the request. ### Instruction: {} ### Input: {} ### Response: {}""" def format_prompts(examples): instructions = examples["instruction"] inputs = examples.get("input", [""] * len(instructions)) outputs = examples["output"] texts = [] for inst, inp, out in zip(instructions, inputs, outputs): text = ALPACA_PROMPT.format(inst, inp, out) + tokenizer.eos_token texts.append(text) return {"text": texts} # 커스텀 데이터셋 (예: AI 서버 구축 Q&A) raw_data = [ { "instruction": "Docker Compose에서 GPU를 사용하는 방법을 설명해줘", "input": "", "output": "Docker Compose에서 GPU를 사용하려면 services 하위에 deploy.resources.reservations.devices 블록을 추가하고..." }, # ... 수백~수천 개의 Q&A 데이터 ] dataset = Dataset.from_list(raw_data).map( format_prompts, batched=True, remove_columns=["instruction", "input", "output"] ) ## ── 4. 학습 설정 ────────────────────────────── trainer = SFTTrainer( model=model, tokenizer=tokenizer, train_dataset=dataset, dataset_text_field="text", max_seq_length=4096, dataset_num_proc=4, packing=True, # 짧은 샘플 패킹으로 학습 효율↑ args=TrainingArguments( per_device_train_batch_size=2, gradient_accumulation_steps=4, # 실질 배치: 2*4 = 8 warmup_steps=10, num_train_epochs=3, learning_rate=2e-4, fp16=not torch.cuda.is_bf16_supported(), bf16=torch.cuda.is_bf16_supported(), logging_steps=10, optim="adamw_8bit", # 8bit Adam: VRAM 절약 weight_decay=0.01, lr_scheduler_type="cosine", output_dir="./outputs/lora_checkpoint", save_steps=100, ) ) # 학습 시작 (RTX 3090, 14B, 1000 샘플 기준 ~2시간) trainer_stats = trainer.train() print(f"학습 완료: {trainer_stats.metrics}") ## ── 5. GGUF 변환 후 Ollama 배포 ────────────── model.save_pretrained_gguf( "lora_model", tokenizer, quantization_method="q4_k_m" # 동시에 GGUF로 저장 ) # Ollama에 배포 import subprocess subprocess.run([ "docker", "exec", "-it", "ollama", "ollama", "create", "my-custom-model", "-f", "/models/lora_model/Modelfile" ])
📊 데이터셋 품질이 파인튜닝 성능을 결정
Flash Attention · KV Cache · 배치 최적화
같은 GPU, 같은 모델이라도 세부 설정에 따라 추론 속도가 2~3배 차이납니다. Flash Attention, KV Cache 최적화, 배치 전략은 추가 비용 없이 성능을 극적으로 개선합니다.
| 최적화 기법 | 효과 | 필요 조건 | 적용 방법 |
|---|---|---|---|
| Flash Attention 2 | 속도 2~4x, VRAM 30% 절약 | CUDA 11.8+, Ampere GPU 이상 | vLLM: 자동 / Ollama: 환경변수 설정 |
| Flash Attention 3 | FA2 대비 추가 1.5~2x | Hopper GPU (H100) 이상 | pip install flash-attn==3.x |
| Prefix Caching | 시스템 프롬프트 재사용 90%↓ | vLLM 0.4+ | –enable-prefix-caching |
| Speculative Decoding | 소형 드래프트 모델로 2~3x 속도 | 보조 모델 필요 | vLLM –speculative-model 옵션 |
| Chunked Prefill | 긴 프롬프트 처리 효율화 | vLLM 0.3+ | –enable-chunked-prefill |
# docker-compose.yml Ollama 환경변수에 추가 environment: - OLLAMA_FLASH_ATTENTION=1 # Flash Attention 활성화 - OLLAMA_NUM_PARALLEL=4 # 동시 요청 처리 수 - OLLAMA_MAX_LOADED_MODELS=2 # 메모리 상주 모델 수 제한 - OLLAMA_KEEP_ALIVE=30m # 미사용 모델 30분 후 언로드 - OLLAMA_MAX_QUEUE=512 # 대기열 최대 크기 # Modelfile에서 모델별 KV Cache 최적화 cat > /tmp/Modelfile << 'EOF' FROM qwen2.5:14b PARAMETER num_ctx 8192 # KV Cache 크기 (컨텍스트 길이) PARAMETER num_batch 512 # 배치 크기 (높을수록 빠름, VRAM↑) PARAMETER num_gpu 48 # GPU 레이어 수 (최대 = 모두 GPU) PARAMETER num_thread 8 # CPU 스레드 (GPU 보조) PARAMETER use_mlock 1 # 메모리 고정 (스왑 방지) PARAMETER use_mmap 1 # 메모리 매핑 활성화 PARAMETER rope_frequency_base 1000000 # RoPE 스케일링 (긴 컨텍스트) EOF docker exec -it ollama ollama create qwen2.5-14b-optimized -f /tmp/Modelfile
멀티 GPU 추론 — 텐서 병렬화로 70B 구동
RTX 3090 두 장(48GB VRAM)으로 70B 모델을 실행하려면 텐서 병렬화(Tensor Parallelism)가 필요합니다. vLLM이 이를 가장 쉽게 지원합니다. 텐서 병렬화는 각 레이어의 가중치 행렬을 여러 GPU에 분할해 동시에 연산합니다.
services:
vllm-70b:
image: vllm/vllm-openai:latest
container_name: vllm_70b
restart: unless-stopped
ports:
- "8003:8000"
volumes:
- huggingface_cache:/root/.cache/huggingface
command: >
--model meta-llama/Llama-3.3-70B-Instruct
--tensor-parallel-size 2 # GPU 2개로 분산
--gpu-memory-utilization 0.92
--max-model-len 8192
--api-key your_api_key
--host 0.0.0.0
--quantization awq # AWQ 양자화 버전 사용
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all # 모든 GPU 사용
capabilities: [gpu]
ipc: host # GPU 간 통신 최적화 (필수!)
shm_size: '32gb'
networks:
- ai_net
networks:
ai_net:
external: true
name: ai_network- 처리량 10배 향상 — vLLM PagedAttention + Continuous Batching으로 동시 100명 사용 가능
- 24GB VRAM에서 70B 구동 — GGUF Q4_K_M + GPU/CPU 혼합으로 느리지만 동작 가능
- 나만의 특화 모델 — Unsloth LoRA로 2~4시간 만에 도메인 전문가 AI 완성
- 추론 속도 3배 향상 — Flash Attention 2 + Prefix Caching + 최적 배치 설정
- 70B 고품질 추론 — 듀얼 GPU 텐서 병렬화로 GPT-4급 로컬 LLM 운영
