사내에서 외부 ChatGPT 사용을 막아둔 곳이 늘고 있다. 계약서나 소스 코드를 붙여넣는 순간 데이터가 밖으로 나가니 보안팀 입장에선 당연한 조치다. 그래서 요즘 "우리 망 안에서만 도는 챗봇을 하나 띄워달라"는 요청을 자주 받는다. 이 글은 그 요청을 받았을 때 제가 실제로 밟는 순서를 그대로 정리한 것이다.
결론부터 말하자면, Ollama로 로컬 LLM을 실행하고 그 앞단에 Open WebUI를 붙이면 외부 API 없이 사내망에서 도는 ChatGPT 형태의 서비스가 완성된다. 여기에 사용자 인증과 권한, 문서 업로드 기반 RAG, Nginx 리버스 프록시와 HTTPS를 얹으면 운영 가능한 수준이 된다. 아래에서 설치부터 프록시 설정까지 차례로 살펴본다.
1. 구성 요소부터 정리
두 개의 프로그램이 역할을 나눠 맡는다. Ollama는 모델을 내려받아 로컬에서 추론을 돌리는 런타임이다. 기본적으로 11434 포트에 REST API를 연다. Open WebUI는 그 API 앞에 붙는 웹 프론트엔드로, ChatGPT와 비슷한 채팅 화면과 사용자 관리, 문서 업로드 같은 운영 기능을 제공한다.
즉 Ollama는 엔진, Open WebUI는 대시보드다. 둘을 한 장비에 같이 올려도 되고, GPU 서버에 Ollama만 두고 Open WebUI는 별도 장비에서 원격 연결해도 된다. 여기서는 한 장비에 둘 다 올리는 방식을 기준으로 잡는다.
2. Ollama 설치와 모델 내려받기
리눅스에서는 설치 스크립트 한 줄이면 끝난다.
$ curl -fsSL https://ollama.com/install.sh | sh >>> Installing ollama to /usr/local >>> Creating ollama user... >>> Creating ollama systemd service... >>> Enabling and starting ollama service...
설치가 끝나면 systemd 서비스로 등록되어 11434 포트에서 바로 뜬다. 모델을 하나 받아 실행해 본다. 여기서는 3B급 소형 모델로 시작한다. 처음부터 큰 모델을 받으면 다운로드만 한참 걸리고, 장비 사양이 안 맞으면 응답이 느려서 테스트가 답답하다.
$ ollama pull llama3.2 pulling manifest pulling 6a0746a1ec1a... 100% 2.0 GB verifying sha256 digest writing manifest success $ ollama list NAME ID SIZE MODIFIED llama3.2:latest a80c4f17acd5 2.0 GB 3 seconds ago
API가 실제로 응답하는지 curl로 확인한다. 이 단계에서 답이 오면 엔진 쪽은 끝난 것이다.
$ curl http://localhost:11434/api/chat -d '{
"model": "llama3.2",
"messages": [{"role": "user", "content": "한 문장으로 자기소개해줘"}],
"stream": false
}'
{"model":"llama3.2","message":{"role":"assistant","content":"저는 로컬에서 동작하는..."},"done":true}
여기서 한 번 걸리는 지점이 있다. Open WebUI를 다른 장비나 컨테이너에서 붙이려면 Ollama가 localhost가 아니라 모든 인터페이스에서 수신해야 한다. 서비스 환경 변수에 OLLAMA_HOST=0.0.0.0를 넣고 재시작한다.
$ sudo systemctl edit ollama # [Service] 아래에 추가 Environment="OLLAMA_HOST=0.0.0.0:11434" $ sudo systemctl restart ollama
단, 이렇게 열면 방화벽에서 11434 포트를 사내망 안으로만 제한해야 한다. 이 포트에는 인증이 없다. 외부에 노출되면 아무나 모델을 호출할 수 있으니 반드시 막는다.
3. Open WebUI 올리기
Open WebUI는 Docker로 올리는 방식이 가장 손이 덜 간다. 이미 Ollama가 같은 호스트에서 돌고 있으니 host.docker.internal로 연결한다.
$ docker run -d -p 3000:8080 \ --add-host=host.docker.internal:host-gateway \ -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \ -e WEBUI_SECRET_KEY=$(openssl rand -hex 32) \ -v open-webui:/app/backend/data \ --name open-webui --restart always \ ghcr.io/open-webui/open-webui:main
포트 매핑 -p 3000:8080은 호스트의 3000번을 컨테이너 내부 8080번으로 넘긴다는 뜻이다. WEBUI_SECRET_KEY는 세션 서명에 쓰는 키라 고정값으로 넣어둬야 컨테이너를 다시 만들어도 로그인이 유지된다. 데이터는 open-webui 볼륨에 남으니 컨테이너를 지워도 사용자와 대화 기록은 보존된다.
Ollama까지 한 컨테이너에 묶고 싶으면 :ollama 태그를 쓰는 올인원 이미지도 있다. 다만 저는 엔진과 프론트를 분리해두는 쪽을 권한다. 모델 다운로드 용량이 크고, GPU 할당이나 재시작 주기가 둘이 다르기 때문이다. 분리해두면 Open WebUI만 업데이트해도 모델을 다시 받을 일이 없다.
브라우저로 http://서버IP:3000에 접속하면 첫 화면이 뜬다. 여기서 맨 처음 가입하는 계정이 곧 관리자가 된다. 이 계정으로 로그인한 뒤 왼쪽 아래 모델 선택 메뉴에 llama3.2가 보이면 연동이 된 것이다. 안 보이면 관리자 설정의 연결(Connections) 항목에서 Ollama URL을 다시 확인한다.
4. 사용자 인증과 권한
사내 서비스라면 아무나 가입해서 바로 쓰게 두면 곤란하다. Open WebUI는 역할(role) 기반으로 접근을 나눈다. 역할은 admin, user, pending 세 가지다. pending은 가입은 됐지만 관리자가 승인하기 전까지 아무 기능도 못 쓰는 대기 상태를 뜻한다.
기본 동작이 잘 설계되어 있다. 첫 사용자만 관리자가 되고 그 뒤로는 회원가입이 자동으로 닫힌다. 새 가입을 다시 열더라도 신규 계정의 기본 역할은 pending이라, 관리자가 승인해야만 로그인이 된다. 이 값들은 환경 변수로 제어한다.
# 회원가입 자체를 닫아두고 관리자가 계정을 직접 만든다 -e ENABLE_SIGNUP=false # 가입은 열되, 신규는 대기 상태로 두고 승인제로 운영한다 -e ENABLE_SIGNUP=true \ -e DEFAULT_USER_ROLE=pending
사내 계정 체계가 이미 있다면 SSO를 붙이는 편이 관리가 깔끔하다. Open WebUI는 OAuth와 OIDC 연동을 지원하니 Keycloak이나 Google Workspace, Authentik 같은 곳에 물릴 수 있다. 처음 도입 단계에서는 승인제(pending)로 시작해 사용자를 손으로 승인하다가, 인원이 늘면 SSO로 넘어가는 순서가 현실적이다.
승인은 관리자 패널의 사용자 목록에서 역할을 pending에서 user로 바꿔주면 끝난다. 모델 접근을 직군별로 나누고 싶으면 그룹을 만들어 특정 모델만 보이게 제한할 수도 있다.
5. 문서 업로드와 RAG
여기서부터가 사내 챗봇의 진짜 값어치다. RAG(Retrieval Augmented Generation)는 모델이 학습하지 않은 사내 문서를 벡터로 색인해 두고, 질문이 들어오면 관련 조각을 찾아 프롬프트에 끼워 넣어 답하게 하는 방식이다. 모델을 다시 학습시키지 않고도 우리 문서 내용을 답하게 만드는 실용적인 방법이다.
Open WebUI에서는 워크스페이스의 지식(Knowledge) 기능으로 문서 묶음을 만든다. PDF나 마크다운, 텍스트 파일을 올리면 내부에서 잘게 쪼개(chunk) 임베딩 모델로 벡터화한 뒤 벡터 DB에 저장한다. 채팅 중에 #을 입력하고 지식 이름을 고르면 그 문서를 참조해서 답한다.
임베딩 모델은 관리자 설정의 문서(Documents) 항목에서 지정한다. 로컬로만 돌리려면 여기서도 Ollama 임베딩 모델을 쓰면 된다. 문서가 외부로 안 나가는 게 핵심 목적이니 임베딩까지 로컬 모델로 맞추는 걸 권한다.
$ ollama pull nomic-embed-text pulling manifest pulling 970aa74c0a90... 100% 274 MB success # 관리자 설정 > 문서 > 임베딩 모델에 nomic-embed-text 지정
답이 엉뚱하게 나오면 청크 크기(Chunk Size)와 Top K 값을 먼저 본다. Top K는 벡터 DB에서 가져올 문서 조각 수인데, 너무 크게 잡으면 모델 컨텍스트 창을 넘겨 오히려 답이 나빠질 수 있다. 반대로 너무 작으면 필요한 근거를 못 가져온다. 문서 성격에 따라 조정하는 값이라 정답은 없고, 몇 번 돌려보며 맞춘다.
6. Nginx 리버스 프록시와 HTTPS
지금까지는 3000 포트로 직접 접속했다. 운영에서는 도메인을 붙이고 HTTPS로 감싼다. Nginx를 앞단에 두고 Open WebUI로 넘긴다. 채팅은 스트리밍 응답이라 WebSocket 업그레이드 헤더를 반드시 넘겨줘야 한다. 이걸 빠뜨리면 응답이 중간에 끊기거나 실시간 출력이 안 된다. 제가 처음 붙일 때 가장 자주 걸리는 지점이 여기다.
server {
listen 80;
server_name chat.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 스트리밍 응답을 위한 WebSocket 업그레이드
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# 긴 응답이 중간에 끊기지 않도록
proxy_read_timeout 600s;
}
}
HTTPS는 certbot으로 발급한다. 사내망 전용이라 외부에서 도메인 검증이 안 되는 경우가 많은데, 그럴 때는 DNS 검증을 쓰거나 사내 CA로 발급한 인증서를 Nginx에 직접 물린다. 인터넷에서 접근 가능한 도메인이라면 아래 한 줄로 끝난다.
$ sudo certbot --nginx -d chat.example.com Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/chat.example.com/fullchain.pem Deploying certificate Successfully deployed certificate for chat.example.com $ curl -I https://chat.example.com HTTP/2 200
이렇게 프록시까지 붙이면 사용자는 3000 포트를 몰라도 되고, https://chat.example.com으로만 접속한다. Open WebUI 컨테이너와 Ollama 포트는 127.0.0.1 또는 사내망으로만 열어두고, 바깥에는 Nginx의 443만 노출한다.
7. 마무리
정리하면 Ollama가 모델을 돌리고, Open WebUI가 채팅 화면과 사용자 관리와 RAG를 맡고, Nginx가 HTTPS로 감싼다. 소형 모델과 승인제 계정으로 작게 시작해 쓰임이 확인되면 큰 모델과 SSO, 그룹별 모델 제한으로 넓혀가는 순서를 권한다.
운영에서 놓치기 쉬운 두 가지만 다시 짚는다. 첫째, Ollama의 11434 포트에는 인증이 없으니 방화벽으로 반드시 막는다. 둘째, Nginx에서 WebSocket 업그레이드 헤더를 넘겨야 스트리밍 응답이 정상 동작한다. 이 둘만 챙기면 사내망 자체 호스팅 챗봇으로 무리 없이 쓸 수 있다.