대상 환경 : Rocky Linux 9.7 · Vanilla Kubernetes · NGINX Ingress · Ceph RBD/CephFS · 외부 PostgreSQL · GPU 없음
규모 : 동시 사용자 ≤ 10명
도메인 :chat.ai.nova.office(TLS는 상단 리버스프록시에서 종단)
향후 확장 : 외부 LLM API (Claude / GPT / Gemini 등) 연동 예정
| 컴포넌트 | 역할 | 이유 |
|---|---|---|
| Ollama | 로컬 LLM 추론 엔진 (CPU) | GGUF 양자화 모델 기반, 설치 단순, 동적 모델 스위칭 지원 |
| LiteLLM | OpenAI-호환 프록시/게이트웨이 | Virtual Key · 스펜드 추적 · 외부 LLM 통합 라우팅 · Rate Limit |
| Open WebUI | 사용자용 챗 UI | RBAC · 채팅 히스토리 · RAG · 관리자 콘솔 |
| PostgreSQL (외부) | 영속 메타데이터 저장 | LiteLLM(키·스펜드) + Open WebUI(사용자·채팅) 공용 |
| Ceph RBD | Ollama 모델 저장 | ReadWriteOnce, 단일 StatefulSet Pod 전용 |
| CephFS | Open WebUI 데이터 저장 | ReadWriteMany 옵션 확보 (향후 replica 확장 대비) |
외부에 이미 배포된 PostgreSQL 에 접속하여 다음을 수행합니다. 비밀번호는 실제 값으로 치환하되 본 문서에는 절대 기록하지 마십시오.
-- 관리자로 접속 (psql)
-- LiteLLM 용
CREATE DATABASE litellm;
CREATE USER litellm_user WITH ENCRYPTED PASSWORD '$LITELLM_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE litellm TO litellm_user;
\c litellm
GRANT ALL ON SCHEMA public TO litellm_user;
ALTER DATABASE litellm OWNER TO litellm_user;
-- Open WebUI 용
CREATE DATABASE openwebui;
CREATE USER openwebui_user WITH ENCRYPTED PASSWORD '$OPENWEBUI_DB_PASSWORD';
GRANT ALL PRIVILEGES ON DATABASE openwebui TO openwebui_user;
\c openwebui
GRANT ALL ON SCHEMA public TO openwebui_user;
ALTER DATABASE openwebui OWNER TO openwebui_user;
PostgreSQL 15 이상에서는
GRANT ALL ON SCHEMA public반드시 필요 (권한 체계 변경)
Ceph 환경에 맞는 StorageClass 명을 먼저 확인합니다.
kubectl get storageclass
# 예시 출력:
# NAME PROVISIONER ...
# ceph-rbd rbd.csi.ceph.com ...
# ceph-filesystem cephfs.csi.ceph.com ...
이후 manifest 의
storageClassName값은 실제 클러스터의 이름으로 치환하십시오.
| 항목 | 예시 값 | 비고 |
|---|---|---|
| Namespace | llm-stack |
신규 생성 |
| PG Host | $POSTGRES_HOST |
기존 PG 서비스 주소 |
| PG Port | 5432 |
|
| PG DB / User (LiteLLM) | litellm / litellm_user |
2.1 에서 생성 |
| PG DB / User (Open WebUI) | openwebui / openwebui_user |
2.1 에서 생성 |
| StorageClass (RBD) | ceph-rbd |
클러스터에 맞게 치환 |
| StorageClass (CephFS) | ceph-filesystem |
클러스터에 맞게 치환 |
| Ingress 도메인 | chat.ai.nova.office |
|
| IngressClass | nginx |
기존 NGINX Ingress |
# 00-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: llm-stack
labels:
name: llm-stack
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/audit: restricted
# 공통 유틸
gen_key() { openssl rand -hex 32; }
# LiteLLM
kubectl -n llm-stack create secret generic litellm-secrets \
--from-literal=LITELLM_MASTER_KEY="sk-$(gen_key)" \
--from-literal=LITELLM_SALT_KEY="$(gen_key)" \
--from-literal=DATABASE_URL="postgresql://litellm_user:${LITELLM_DB_PASSWORD}@${POSTGRES_HOST}:5432/litellm" \
--from-literal=UI_USERNAME="admin" \
--from-literal=UI_PASSWORD="$(gen_key)"
# 외부 LLM API Key (향후 연동용 — 지금은 빈 값으로 생성만 해둠)
kubectl -n llm-stack create secret generic litellm-provider-keys \
--from-literal=ANTHROPIC_API_KEY="" \
--from-literal=OPENAI_API_KEY="" \
--from-literal=GEMINI_API_KEY=""
# Open WebUI
kubectl -n llm-stack create secret generic openwebui-secrets \
--from-literal=WEBUI_SECRET_KEY="$(gen_key)" \
--from-literal=DATABASE_URL="postgresql://openwebui_user:${OPENWEBUI_DB_PASSWORD}@${POSTGRES_HOST}:5432/openwebui" \
--from-literal=OPENAI_API_KEY="<LiteLLM master key or virtual key 값>"
LITELLM_SALT_KEY는 모델 등록 후 절대 변경 금지 (DB 내 자격증명 암호화 키)- Open WebUI 의
OPENAI_API_KEY는 LiteLLM의 master key 또는 발급된 virtual key 값과 반드시 일치해야 함
# 10-litellm-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: litellm-config
namespace: llm-stack
data:
config.yaml: |
model_list:
# ── 로컬 Ollama 모델 ──────────────────────────
- model_name: llama3.2-3b
litellm_params:
model: ollama_chat/llama3.2:3b-instruct-q4_K_M
api_base: http://ollama:11434
timeout: 600
- model_name: qwen2.5-7b
litellm_params:
model: ollama_chat/qwen2.5:7b-instruct-q4_K_M
api_base: http://ollama:11434
timeout: 600
# ── 외부 LLM API (향후 주석 해제 후 사용) ─────
# - model_name: claude-sonnet
# litellm_params:
# model: anthropic/claude-sonnet-4-5
# api_key: os.environ/ANTHROPIC_API_KEY
# - model_name: gpt-4o
# litellm_params:
# model: openai/gpt-4o
# api_key: os.environ/OPENAI_API_KEY
# - model_name: gemini-2.5-pro
# litellm_params:
# model: gemini/gemini-2.5-pro
# api_key: os.environ/GEMINI_API_KEY
litellm_settings:
drop_params: true
request_timeout: 600
set_verbose: false
# 장애 시 대체 모델 라우팅 (예시 — 외부 연동 후 활성화)
# fallbacks:
# - llama3.2-3b: ["qwen2.5-7b"]
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
# 운영 안정성
proxy_batch_write_at: 60
database_connection_pool_limit: 10
database_connection_timeout: 60
disable_spend_logs: false
disable_error_logs: false
# 봇/크롤러 차단
block_robots: true
router_settings:
routing_strategy: simple-shuffle
num_retries: 2
timeout: 600
# 20-ollama.yaml
apiVersion: v1
kind: Service
metadata:
name: ollama
namespace: llm-stack
labels:
app: ollama
spec:
type: ClusterIP
selector:
app: ollama
ports:
- name: http
port: 11434
targetPort: 11434
protocol: TCP
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: ollama
namespace: llm-stack
spec:
serviceName: ollama
replicas: 1
selector:
matchLabels:
app: ollama
template:
metadata:
labels:
app: ollama
spec:
securityContext:
runAsNonRoot: false # Ollama 공식 이미지는 root 필요
seccompProfile:
type: RuntimeDefault
containers:
- name: ollama
image: ollama/ollama:0.5.12 # 운영 전 최신 stable 확인 후 pin
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 11434
env:
- name: OLLAMA_HOST
value: "0.0.0.0:11434"
- name: OLLAMA_KEEP_ALIVE
value: "30m" # 모델 메모리 유지 시간
- name: OLLAMA_NUM_PARALLEL
value: "2" # 동시 요청 처리 (CPU 기준 보수적)
- name: OLLAMA_MAX_LOADED_MODELS
value: "2" # 동시 로드 모델 수
- name: OLLAMA_NUM_THREAD
value: "0" # 0 = 자동 감지
resources:
requests:
cpu: "2"
memory: "6Gi"
limits:
cpu: "6"
memory: "12Gi" # 7B Q4 모델 실행 시 5~8GB 상주
volumeMounts:
- name: ollama-data
mountPath: /root/.ollama
readinessProbe:
httpGet:
path: /
port: 11434
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /
port: 11434
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 5
startupProbe:
httpGet:
path: /
port: 11434
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30 # 최대 5분 startup 허용
volumeClaimTemplates:
- metadata:
name: ollama-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ceph-rbd # ← 실제 RBD StorageClass 명으로 치환
resources:
requests:
storage: 50Gi
kubectl -n llm-stack exec -it ollama-0 -- \
ollama pull llama3.2:3b-instruct-q4_K_M
kubectl -n llm-stack exec -it ollama-0 -- \
ollama pull qwen2.5:7b-instruct-q4_K_M
# 확인
kubectl -n llm-stack exec -it ollama-0 -- ollama list
# 30-litellm.yaml
apiVersion: v1
kind: Service
metadata:
name: litellm
namespace: llm-stack
spec:
type: ClusterIP
selector:
app: litellm
ports:
- name: http
port: 4000
targetPort: 4000
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: litellm
namespace: llm-stack
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: litellm
template:
metadata:
labels:
app: litellm
annotations:
# config.yaml 변경 시 pod 자동 재기동 유도 — 변경 시 값 업데이트
configHash: "v1"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: litellm
image: ghcr.io/berriai/litellm-database:main-stable
imagePullPolicy: IfNotPresent
args:
- "--config"
- "/app/config.yaml"
- "--port"
- "4000"
- "--num_workers"
- "2"
ports:
- name: http
containerPort: 4000
env:
- name: LITELLM_LOG
value: "INFO"
# DB 스키마 업데이트 허용 (첫 기동 시 migration)
- name: DISABLE_SCHEMA_UPDATE
value: "false"
# ConfigMap 에서 os.environ 으로 참조되는 Secret 주입
- name: LITELLM_MASTER_KEY
valueFrom:
secretKeyRef: { name: litellm-secrets, key: LITELLM_MASTER_KEY }
- name: LITELLM_SALT_KEY
valueFrom:
secretKeyRef: { name: litellm-secrets, key: LITELLM_SALT_KEY }
- name: DATABASE_URL
valueFrom:
secretKeyRef: { name: litellm-secrets, key: DATABASE_URL }
- name: UI_USERNAME
valueFrom:
secretKeyRef: { name: litellm-secrets, key: UI_USERNAME }
- name: UI_PASSWORD
valueFrom:
secretKeyRef: { name: litellm-secrets, key: UI_PASSWORD }
envFrom:
# 외부 LLM Provider Key 전체 주입 (향후 확장)
- secretRef:
name: litellm-provider-keys
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumeMounts:
- name: config
mountPath: /app/config.yaml
subPath: config.yaml
readOnly: true
readinessProbe:
httpGet:
path: /health/readiness
port: 4000
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/liveliness
port: 4000
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 5
startupProbe:
httpGet:
path: /health/readiness
port: 4000
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 30
volumes:
- name: config
configMap:
name: litellm-config
litellm-database이미지는 Prisma client 사전 빌드본 → 기동 시간 단축- LiteLLM 관리 UI :
http://litellm:4000/ui(내부 전용 또는 별도 Ingress 추가 시)
# 40-openwebui-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: openwebui-data
namespace: llm-stack
spec:
accessModes: ["ReadWriteMany"] # 향후 replica 확장 대비
storageClassName: ceph-filesystem # ← 실제 CephFS StorageClass 명으로 치환
resources:
requests:
storage: 10Gi
# 41-openwebui.yaml
apiVersion: v1
kind: Service
metadata:
name: open-webui
namespace: llm-stack
spec:
type: ClusterIP
selector:
app: open-webui
ports:
- name: http
port: 80
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: open-webui
namespace: llm-stack
spec:
replicas: 1
strategy:
type: Recreate # SQLite fallback 방지 및 PVC 충돌 회피
selector:
matchLabels:
app: open-webui
template:
metadata:
labels:
app: open-webui
spec:
securityContext:
runAsNonRoot: false
fsGroup: 0
seccompProfile:
type: RuntimeDefault
containers:
- name: open-webui
image: ghcr.io/open-webui/open-webui:v0.8.6
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
env:
# LiteLLM 경유 모델만 사용 (Ollama 직접 연결 비활성화)
- name: ENABLE_OLLAMA_API
value: "false"
- name: OPENAI_API_BASE_URL
value: "http://litellm:4000/v1"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef: { name: openwebui-secrets, key: OPENAI_API_KEY }
# 외부 PostgreSQL
- name: DATABASE_URL
valueFrom:
secretKeyRef: { name: openwebui-secrets, key: DATABASE_URL }
# 세션/JWT 서명 키
- name: WEBUI_SECRET_KEY
valueFrom:
secretKeyRef: { name: openwebui-secrets, key: WEBUI_SECRET_KEY }
# 도메인 / 프록시 대응
- name: WEBUI_URL
value: "https://chat.ai.nova.office"
- name: FORWARDED_ALLOW_IPS
value: "*"
# 내부 테스트망 — 가입 허용 여부에 따라 조정
- name: ENABLE_SIGNUP
value: "true"
- name: DEFAULT_USER_ROLE
value: "pending" # 관리자 승인 필요
# 원격 OpenAI 설정 변경은 관리자만
- name: ENABLE_ADMIN_EXPORT
value: "true"
- name: ENABLE_ADMIN_CHAT_ACCESS
value: "false"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumeMounts:
- name: data
mountPath: /app/backend/data
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 90
periodSeconds: 30
timeoutSeconds: 10
failureThreshold: 5
startupProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 60 # migration 최대 10분 허용
volumes:
- name: data
persistentVolumeClaim:
claimName: openwebui-data
# 42-openwebui-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: open-webui
namespace: llm-stack
annotations:
# TLS 는 상단 리버스프록시에서 종단 → Ingress 에서 리다이렉트 금지
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/force-ssl-redirect: "false"
# WebSocket / Socket.IO 지원 (Open WebUI 실시간 업데이트)
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-connect-timeout: "60"
# 파일 업로드 (RAG 문서 등)
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
# 상단 프록시가 전달하는 X-Forwarded-* 를 그대로 전달
nginx.ingress.kubernetes.io/use-forwarded-headers: "true"
spec:
ingressClassName: nginx
rules:
- host: chat.ai.nova.office
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: open-webui
port:
number: 80
상단 리버스프록시 설정 시 체크
X-Forwarded-Proto: https반드시 전달X-Forwarded-For전달- WebSocket
Upgrade/Connection헤더 전달proxy_read_timeout60s 이상 (streaming 응답)
# 50-networkpolicy.yaml
---
# 기본 Deny-All
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: llm-stack
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
# DNS 허용 (모든 Pod)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: llm-stack
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# Open WebUI: Ingress Controller 로부터 수신 허용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: openwebui-allow-ingress
namespace: llm-stack
spec:
podSelector:
matchLabels:
app: open-webui
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 8080
---
# Open WebUI → LiteLLM
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: openwebui-egress
namespace: llm-stack
spec:
podSelector:
matchLabels:
app: open-webui
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels:
app: litellm
ports:
- protocol: TCP
port: 4000
# 외부 PostgreSQL (주소/포트는 환경에 맞게 별도 CIDR 제한 권장)
- ports:
- protocol: TCP
port: 5432
---
# LiteLLM → Ollama + PostgreSQL + (향후) 외부 LLM
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: litellm-ingress
namespace: llm-stack
spec:
podSelector:
matchLabels:
app: litellm
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app: open-webui
ports:
- protocol: TCP
port: 4000
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: litellm-egress
namespace: llm-stack
spec:
podSelector:
matchLabels:
app: litellm
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels:
app: ollama
ports:
- protocol: TCP
port: 11434
# PostgreSQL
- ports:
- protocol: TCP
port: 5432
# 향후 외부 LLM API (HTTPS)
- ports:
- protocol: TCP
port: 443
---
# Ollama: LiteLLM 로부터만 수신 허용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ollama-ingress
namespace: llm-stack
spec:
podSelector:
matchLabels:
app: ollama
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app: litellm
ports:
- protocol: TCP
port: 11434
---
# Ollama: 모델 pull 시 HTTPS egress 필요 (registry.ollama.ai)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ollama-egress
namespace: llm-stack
spec:
podSelector:
matchLabels:
app: ollama
policyTypes: [Egress]
egress:
- ports:
- protocol: TCP
port: 443
외부 PostgreSQL 전용 CIDR 이 있다면
egress.to[].ipBlock.cidr로 제한하세요. 완전 격리망이라면 Ollama egress 443 은 모델 pull 작업 시에만 열고 평시 차단 권장.
# 1) 네임스페이스 & Secret
kubectl apply -f 00-namespace.yaml
# 3.2 절의 kubectl create secret 명령 3개 실행
# 2) Config
kubectl apply -f 10-litellm-configmap.yaml
# 3) Ollama 먼저 기동 (startup 시간 감안)
kubectl apply -f 20-ollama.yaml
kubectl -n llm-stack rollout status statefulset/ollama --timeout=300s
# 4) 모델 사전 pull
kubectl -n llm-stack exec -it ollama-0 -- ollama pull llama3.2:3b-instruct-q4_K_M
kubectl -n llm-stack exec -it ollama-0 -- ollama pull qwen2.5:7b-instruct-q4_K_M
# 5) LiteLLM (최초 기동 시 DB migration 수행)
kubectl apply -f 30-litellm.yaml
kubectl -n llm-stack rollout status deployment/litellm --timeout=300s
# 6) Open WebUI
kubectl apply -f 40-openwebui-pvc.yaml
kubectl apply -f 41-openwebui.yaml
kubectl apply -f 42-openwebui-ingress.yaml
kubectl -n llm-stack rollout status deployment/open-webui --timeout=600s
# 7) NetworkPolicy (동작 확인 후 마지막에 적용 권장)
kubectl apply -f 50-networkpolicy.yaml
# port-forward 로 검증
kubectl -n llm-stack port-forward svc/litellm 4000:4000
# 다른 터미널
curl -s http://localhost:4000/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" | jq .
curl -s http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "llama3.2-3b",
"messages": [{"role":"user","content":"안녕하세요"}]
}' | jq .
http://litellm:4000/ui 접속 (port-forward 또는 내부 전용 Ingress 추가)UI_USERNAME / UI_PASSWORD 로 로그인openwebui-secrets 의 OPENAI_API_KEY 업데이트 → Open WebUI pod 재기동https://chat.ai.nova.office 접속# 1) Provider Key 업데이트
kubectl -n llm-stack delete secret litellm-provider-keys
kubectl -n llm-stack create secret generic litellm-provider-keys \
--from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_KEY" \
--from-literal=OPENAI_API_KEY="$OPENAI_KEY" \
--from-literal=GEMINI_API_KEY="$GEMINI_KEY"
# 2) ConfigMap 편집 — 외부 모델 블록 주석 해제
kubectl -n llm-stack edit cm litellm-config
# 3) annotation 해시 변경으로 pod 재기동 유도
kubectl -n llm-stack patch deployment litellm \
-p "{\"spec\":{\"template\":{\"metadata\":{\"annotations\":{\"configHash\":\"v$(date +%s)\"}}}}}"
대규모/GitOps 환경이라면 직접 manifest 대신 공식 Helm Chart 사용도 권장됩니다.
# LiteLLM (BETA)
helm pull oci://ghcr.io/berriai/litellm-helm
helm install litellm ./litellm-helm-<ver>.tgz \
-n llm-stack -f litellm-values.yaml
# Open WebUI (안정)
helm repo add open-webui https://open-webui.github.io/helm-charts
helm repo update
helm install open-webui open-webui/open-webui \
-n llm-stack -f openwebui-values.yaml
openwebui-values.yaml 핵심 :
replicaCount: 1
ollama:
enabled: false # 별도 StatefulSet 사용
pipelines:
enabled: false
tika:
enabled: false # 필요 시 true
databaseUrl: "postgresql://openwebui_user:$PW@$POSTGRES_HOST:5432/openwebui"
persistence:
enabled: true
storageClass: ceph-filesystem
accessModes: [ReadWriteMany]
size: 10Gi
extraEnvVars:
- name: OPENAI_API_BASE_URL
value: "http://litellm:4000/v1"
- name: ENABLE_OLLAMA_API
value: "false"
ingress:
enabled: true
class: nginx
host: chat.ai.nova.office
tls: false # 상단 프록시 종단
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "false"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
내부망과 동일 스택을 개인 PC에서 테스트하려는 경우용. Windows 는 Docker Desktop + WSL2, macOS 는 Docker Desktop, Linux 는 Docker Engine 기준.
llm-local/
├── docker-compose.yml
├── .env # 비밀값 (git 제외)
└── litellm/
└── config.yaml
.env# $PASSWORD 형태로 값 치환 — 실제 파일에는 평문 금지
LITELLM_MASTER_KEY=sk-$GENERATED
LITELLM_SALT_KEY=$GENERATED
POSTGRES_PASSWORD=$GENERATED
WEBUI_SECRET_KEY=$GENERATED
litellm/config.yamlk8s 버전 3.3 절과 동일. 단, api_base 만 http://ollama:11434 로 유지.
docker-compose.ymlservices:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: llmuser
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_MULTIPLE_DATABASES: litellm,openwebui
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U llmuser"]
interval: 10s
timeout: 5s
retries: 5
ollama:
image: ollama/ollama:0.5.12
restart: unless-stopped
volumes:
- ollama:/root/.ollama
environment:
OLLAMA_KEEP_ALIVE: "30m"
OLLAMA_NUM_PARALLEL: "2"
OLLAMA_MAX_LOADED_MODELS: "2"
# 초기 1회: docker exec -it llm-local-ollama-1 ollama pull llama3.2:3b-instruct-q4_K_M
litellm:
image: ghcr.io/berriai/litellm-database:main-stable
restart: unless-stopped
depends_on:
postgres: { condition: service_healthy }
ollama: { condition: service_started }
environment:
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
LITELLM_SALT_KEY: ${LITELLM_SALT_KEY}
DATABASE_URL: postgresql://llmuser:${POSTGRES_PASSWORD}@postgres:5432/litellm
volumes:
- ./litellm/config.yaml:/app/config.yaml:ro
command: ["--config", "/app/config.yaml", "--port", "4000", "--num_workers", "2"]
ports:
- "4000:4000" # 관리 UI 접근용
open-webui:
image: ghcr.io/open-webui/open-webui:v0.8.6
restart: unless-stopped
depends_on:
litellm: { condition: service_started }
postgres: { condition: service_healthy }
environment:
ENABLE_OLLAMA_API: "false"
OPENAI_API_BASE_URL: "http://litellm:4000/v1"
OPENAI_API_KEY: ${LITELLM_MASTER_KEY}
DATABASE_URL: postgresql://llmuser:${POSTGRES_PASSWORD}@postgres:5432/openwebui
WEBUI_SECRET_KEY: ${WEBUI_SECRET_KEY}
WEBUI_URL: "http://localhost:3000"
volumes:
- openwebui:/app/backend/data
ports:
- "3000:8080"
volumes:
pgdata:
ollama:
openwebui:
POSTGRES_MULTIPLE_DATABASES를 쓰려면postgres이미지에 init script 가 필요합니다. 간단히 하려면 DB 하나만 쓰고 LiteLLM / Open WebUI 에 각각 다른 schema 를 할당하거나, 별도 compose 블록으로psql로 DB 2개를 만드는 init container 를 추가하십시오.
docker compose up -d
docker compose exec ollama ollama pull llama3.2:3b-instruct-q4_K_M
docker compose exec ollama ollama pull qwen2.5:7b-instruct-q4_K_M
# 브라우저: http://localhost:3000
| 상황 | 권장 조합 | 비고 |
|---|---|---|
| 가장 단순, 개인용 | Ollama 단독 + Open WebUI | LiteLLM 제거. 팀 API 게이트웨이 불필요 시 최적 |
| Apple Silicon | Ollama (MLX 백엔드) + Open WebUI | Ollama 0.19+ MLX 지원. vLLM은 Mac 미성숙 |
| CPU, 최고 속도 | llama.cpp server + Open WebUI | Ollama 대비 15~25% 빠름, 메모리 적음 |
| GPU + 다수 동시 사용자 | vLLM + LiteLLM + Open WebUI | PagedAttention 으로 throughput 3~16배 |
| 비개발자 단독 사용 | LM Studio 또는 Jan | GUI 완비, 더블클릭 설치 |
현재 환경(GPU 없음 / 10명 이하 / 외부 LLM 연동 예정)에는 Ollama + LiteLLM + Open WebUI 가 최적. vLLM 은 GPU 전제라 이번 환경에 부적합.
kubectl -n llm-stack get pods,svc,ingress,pvc
kubectl -n llm-stack logs -f deploy/litellm
kubectl -n llm-stack logs -f deploy/open-webui
kubectl -n llm-stack logs -f statefulset/ollama
# LiteLLM 내부 헬스체크
kubectl -n llm-stack exec deploy/litellm -- curl -s localhost:4000/health
| 데이터 | 위치 | 백업 방법 |
|---|---|---|
| LiteLLM 메타 (Virtual Key, 스펜드) | 외부 PG litellm DB |
pg_dump litellm |
| Open WebUI 메타 (사용자, 채팅) | 외부 PG openwebui DB |
pg_dump openwebui |
| Open WebUI 파일 (업로드/RAG) | CephFS PVC | Ceph snapshot 또는 파일 레벨 백업 |
| Ollama 모델 | Ceph RBD PVC | 모델 재다운로드 가능 → 백업 불필요 (선택) |
| Secret | kubectl get secret -o yaml |
Sealed Secrets / External Secrets 권장 |
| 컴포넌트 | 주의 |
|---|---|
| LiteLLM | - 업그레이드 전 pg_dump 필수- LITELLM_SALT_KEY 변경 금지- 2026-03 공급망 이슈 버전(1.82.7/1.82.8) 회피, 1.83.0+ 사용 |
| Open WebUI | - UVICORN_WORKERS>1 운영 시 migration 은 workers=1 로 최초 기동 후 재기동 필요- replica>1 로 스케일 아웃 시 Redis + 객체스토리지 필수 |
| Ollama | - 모델 포맷 호환 유지 (GGUF) - 신규 버전 릴리즈 노트 확인 후 pin |
| 증상 | 원인 / 조치 |
|---|---|
| Open WebUI 에서 모델 목록 비어있음 | OPENAI_API_KEY 가 LiteLLM master_key/virtual key 와 불일치 → Secret 갱신 후 pod 재시작 |
LiteLLM 기동 실패 prisma error |
DB 접근 권한 미비. 2.1 의 GRANT ALL ON SCHEMA public 누락 여부 확인 |
| Ollama 응답 매우 느림 / 타임아웃 | CPU-only 에서 7B 이상 모델 사용 시 정상. request_timeout 600s 유지. 경량 모델(3B)로 우선 테스트 |
| Open WebUI 로그인 후 무한 로딩 | WebSocket 미통과. Ingress annotation 의 proxy-http-version: 1.1 + 상단 프록시의 Upgrade 헤더 전달 확인 |
502 Bad Gateway |
proxy-read-timeout 부족. 3600 이상으로 상향 |
모델 pull 시 TLS handshake timeout |
NetworkPolicy 에서 Ollama egress 443 미허용 또는 내부망 방화벽 차단 |
chat.ai.nova.office 에 대해 기본 인증 또는 SSO(OIDC) 추가 고려ENABLE_SIGNUP: false 로 전환 후 관리자가 계정 발급하는 운영도 가능master_key 는 직접 애플리케이션에 노출하지 말고 virtual key 발급하여 사용?sslmode=require) 권장| 항목 | 최소 | 권장 |
|---|---|---|
| Worker Node | 4 vCPU / 8GB RAM | 8 vCPU / 16GB RAM 이상 |
| Ollama Pod 메모리 | 6Gi (3B 모델만) | 12Gi (7B Q4 포함) |
| 디스크 (Ollama) | 10Gi | 50Gi (다모델 실험) |
| 디스크 (Open WebUI) | 5Gi | 10Gi |
| 네트워크 | 1Gbps 내부 | 1Gbps+ |
공식 문서
공식 리포지토리
참고 (Priority 2~4)
문서 버전 : v1.0 · 작성 기준일: 2026-04
확인/검증 권장 : Ollama / Open WebUI / LiteLLM 각 프로젝트 릴리즈 노트 확인 후 이미지 tag 재pin