본문 바로가기
컴퓨터 활용(한글, 오피스 등)/기타

폐쇄망 RAG 시스템 구축 가이드2 (WSL2 Ubuntu + Docker + Qwen 3 8B + llama.cpp)

by 3604 2026. 8. 11.
728x90
[구축 조건]
첨부파일에서 디렉토리 구조는 첨부 이미지와 같다.
그리고 환경파일과 소스파일이 있다.
이 환경을 llm과 연결한 rag시스템을 구성하고 싶다.
사전 설치 등 필요한 내용을 모두 it초보자도 알 수 있도록 단계적으로 친절하게 쉽게 설명해줘.
단, 폐쇄망(로컬pc os windows11에 wsl2에 ubuntu24설치하고 서버로 사용하고, 클라이언트는 windows11이 됨, 서버는 docker 구성으로함)에서 사용하는 rag시스템이다.
openai 대신 Qwen 3 8B를 설치해서 사용할 수 있도록 구성하는 방법을 알려줘 .
하드웨어 제약을 해결하는 방안으로 llama.cpp를 사용할 수 있다면 ollma 보다 llama.cpp 컴파일, 설치, 사용 방법 등을 단계별로 쉽고 친절하게 알려줘.
 

폐쇄망 RAG 시스템 구축 가이드 (WSL2 Ubuntu + Docker + Qwen 3 8B + llama.cpp)

이 가이드는 Windows 11의 WSL2 Ubuntu 환경에서 Docker 기반의 RAG 시스템을 구축하고, OpenAI 대신 로컬 Qwen 3 8B 모델을 llama.cpp로 실행하는 방법을 단계별로 설명합니다.


📋 목차

  1. WSL2 Ubuntu 설치 및 설정
  2. 필수 패키지 설치
  3. llama.cpp 설치 및 컴파일
  4. Qwen 3 8B 모델 다운로드
  5. llama.cpp 서버 실행
  6. Docker 및 PostgreSQL 설정
  7. Rust/Leptos 애플리케이션 수정
  8. 애플리케이션 빌드 및 실행
  9. 문제 해결 가이드

1. WSL2 Ubuntu 설치 및 설정

1.1 Windows 11에서 WSL2 활성화

Windows PowerShell (관리자 권한) 실행:

powershell
# WSL 기능 활성화
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart

# 가상 머신 플랫폼 활성화
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart

# 재부팅 후 WSL2 기본 설정
wsl --set-default-version 2

1.2 Ubuntu 24.04 설치

powershell
# Microsoft Store에서 Ubuntu 24.04 설치 또는 명령어로 설치
wsl --install -d Ubuntu-24.04

1.3 Ubuntu 초기 설정

bash
# Ubuntu 실행
wsl -d Ubuntu-24.04

# 패키지 목록 업데이트
sudo apt update && sudo apt upgrade -y

# 기본 사용자 설정 (선택사항)
sudo passwd root

2. 필수 패키지 설치

2.1 개발 도구 설치

bash
# 빌드 필수 도구 설치
sudo apt update
sudo apt install -y build-essential cmake git curl wget

# Python 및 pip 설치
sudo apt install -y python3 python3-pip

# 기타 유틸리티
sudo apt install -y htop net-tools

# CMake 설치
sudo apt install -y cmake build-essential 
# 버전 확인
cmake --version

2.2 Rust 설치

bash
# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 설치 중 1번 선택 (기본 설치)

# 환경 변수 적용
source ~/.cargo/env

# Rust 버전 확인
rustc --version

2.3 Docker 설치

bash
# Docker 설치 스크립트
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Docker Compose 설치
sudo apt install -y docker-compose-plugin

# 현재 사용자를 docker 그룹에 추가 (sudo 없이 실행)
sudo usermod -aG docker $USER

# WSL2 재시작 필요
exit
wsl --shutdown
wsl -d Ubuntu-24.04

# Docker 서비스 확인
docker --version
docker-compose --version

3. llama.cpp 설치 및 컴파일

3.1 소스 코드 다운로드

bash
# 홈 디렉토리로 이동
cd ~

# llama.cpp 저장소 클론
git clone https://github.com/ggerganov/llama.cpp.git

# 현재 디렉토리 확인 (기존 위치 유지)
cd /home/linux/llama.cpp
pwd  # /home/linux/llama.cpp 확인

# 빌드 디렉토리 생성 및 이동
mkdir -p build
cd build

3.2 CPU 버전 컴파일 (WSL2 기본)

bash
# 기본 CPU 버전 CMake 설정
cmake ..
make -j$(nproc)

# 또는 모든 코어 사용하여 빌드
make -j$(nproc)

# 컴파일 확인
./llama-cli --help

3.3 GPU 가속 버전 (CUDA 지원 시)

bash
# WSL2에서 CUDA 지원 확인
nvidia-smi

# CUDA 설치 (WSL2용)
sudo apt install -y nvidia-cuda-toolkit

# CUDA 버전 CMake 설정
cmake .. -DLLAMA_CUDA=ON
make -j$(nproc)

3.4 llama.cpp 서버 실행 파일 확인

bash
# 빌드된 실행 파일 확인
ls -la bin/

# 서버 실행 파일 확인
ls -la llama-server
# 권한 설정
chmod +x llama-server

# llama-server 실행 파일 확인
./bin/llama-server --help

 

4. 전체 디렉토리 구조

text
/home/linux/
├── llama.cpp/                 # llama.cpp 소스 코드
│   ├── build/                 # CMake 빌드 디렉토리 (새로 생성)
│   │   └── bin/              # 실행 파일 위치
│   │       ├── llama-server  # 서버 실행 파일
│   │       ├── llama-cli     # CLI 실행 파일
│   │       └── ...           # 기타 유틸리티
│   ├── src/                  # 소스 코드
│   ├── CMakeLists.txt        # CMake 설정 파일
│   └── ...
├── models/                   # 모델 저장 디렉토리
│   └── qwen2.5-7b-instruct-q4_k_m.gguf
└── my-rag-app/              # Leptos 애플리케이션
    ├── Cargo.toml          # Rust 패키지 및 의존성 정의
    ├── Dockerfile          # Leptos 멀티스테이지 빌드 정의
    ├── docker-compose.yml  # PostgreSQL+pgvector 및 Leptos 컨테이너 오케스트레이션
    ├── init.sql            # DB 초기화 스크립트 (pgvector 활성화 & 테이블 생성)
    ├── .env                # 환경 변수 (DB URL, OpenAI API Key)
    ├── .gitignore          # 
    ├── public               # 정적 파비콘이나 이미지 파일 등을 담을 public 폴더
    ├── style                 # css 파일 등 
    └── src/
        ├── main.rs         # Axum 백엔드 진입점
        ├── app.rs          # Leptos UI & RAG Server Function
        └── lib.rs          # Leptos UI & RAG Server Function
 

 

5. Docker를 고려한 시스템 설정

5.1 Docker Compose 파일 수정

/home/linux/my-rag-app/docker-compose.yml:

yaml
services:
  db:
    image: pgvector/pgvector:pg16
    container_name: rag_postgres
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: ragdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql

  # 임베딩 서버 (옵션)
  embedding:
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.2
    container_name: embedding_server
    restart: always
    ports:
      - "8081:80"
    environment:
      - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
    volumes:
      - ./embeddings_cache:/data

  # llama.cpp 서버 (Docker로 실행하려는 경우)
  llama:
    image: ghcr.io/ggml-org/llama.cpp:server
    container_name: llama_server
    restart: always
    ports:
      - "8080:8080"
    volumes:
      - /home/linux/models:/models
    command: 
      - -m /models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf
      - -c 4096
      - --host 0.0.0.0
      - --port 8080
    deploy:
      resources:
        limits:
          memory: 8G

volumes:
  pgdata:

5.2 .env 파일 설정

/home/linux/my-rag-app/.env:

env
# PostgreSQL 연결 (Docker 내부에서 접근)
#DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
DATABASE_URL=postgres://postgres:postgres@0.0.0.0:5432/ragdb

# 로컬 LLM 서버 URL (Docker 내부에서 접근)
LLM_SERVER_URL=http://llama:8080

# 또는 WSL2에서 직접 실행 시
# LLM_SERVER_URL=http://host.docker.internal:8080

# 임베딩 서버 URL
EMBED_SERVER_URL=http://embedding:80

# Rust 설정
RUST_LOG=info

6. Docker와 WSL2 연동

6.1 Docker Desktop with WSL2

Windows에서 Docker Desktop이 WSL2와 통합되어 있습니다:

  1. Docker Desktop 설정:
    • Settings → Resources → WSL Integration
    • Ubuntu-24.04 활성화
  2. WSL2에서 Docker 명령어 사용:
  3. bash
    # Docker 명령어 확인
    docker --version
    docker compose --version

6.2 네트워크 설정

bash
# WSL2 IP 확인
ip addr show eth0 | grep inet

# Windows에서 WSL2 서비스 접근을 위한 포트 포워딩 (필요시)
# Windows PowerShell (관리자)
netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 connectport=8080 connectaddress=<WSL2_IP>

 

6.3 Docker 서비스 실행

bash
# 1. my-rag-app 디렉토리로 이동
cd /home/linux/my-rag-app

# 2. Docker Compose 실행
docker compose up -d --remove-orphans

# 3. 상태 확인
docker compose ps
docker compose logs

6.4 Leptos 앱 실행

bash
# 1. my-rag-app 디렉토리로 이동
cd /home/linux/my-rag-app

# 2. 앱 빌드 및 실행
cargo leptos watch

# 또는
cargo leptos serve --release

 


📝 최종 확인

서버 실행 확인(Windows에서 접속)

bash
# WSL2 IP 확인
ip addr show eth0 | grep inet

# Windows PowerShell에서 테스트, llama.cpp 서버 확인
curl http://<WSL2_IP>:8080/completion `
  -H "Content-Type: application/json" `
  -d '{"prompt": "안녕하세요!", "temperature": 0.7, "max_tokens": 100}'
  
# Docker 서비스 확인
docker compose ps

# Leptos 앱 접속
# Windows 브라우저: http://localhost:3000

 


 

[참고] 비docker 방식 llama.cpp 빌드 및 서버 실행 순서

llama.cpp 빌드 및 서버 실행

CPU 버전 서버 실행

bash
# 1. 빌드 디렉토리 생성
cd /home/linux/llama.cpp
mkdir -p build && cd build

# 2. CMake 설정 및 빌드
cmake ..
make -j$(nproc)

# 3. 서버 실행
./bin/llama-server \
  -m /home/linux/models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf \
  -c 4096 \
  -ngl 0 \
  --host 0.0.0.0 \
  --port 8080

 

GPU 버전 서버 실행

bash
# GPU 버전 (CUDA, ngl = GPU 레이어 수)
./bin/llama-server \
  -m /home/linux/models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf \
  -c 4096 \
  -ngl 35 \
  --host 0.0.0.0 \
  --port 8080 \
  --threads 4

 

쉘 스크립트로 자동 실행 (편의성)

실행 스크립트 생성

bash
# 스크립트 파일 생성
nano /home/linux/run_llama_server.sh
bash
#!/bin/bash
# llama.cpp 서버 실행 스크립트

cd /home/linux/llama.cpp/build

# 모델 경로
MODEL="/home/linux/models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf"

# CPU 버전
./bin/llama-server \
  -m "$MODEL" \
  -c 4096 \
  -ngl 0 \
  --host 0.0.0.0 \
  --port 8080 \
  --threads 8
bash
# 스크립트 실행 권한 부여
chmod +x /home/linux/run_llama_server.sh

# 스크립트 실행
/home/linux/run_llama_server.sh

 

 


[참고] 비docker 방식 서비스 자동 실행 (systemd)

llama.cpp 서비스 등록

bash
sudo nano /etc/systemd/system/llama-server.service
ini
[Unit]
Description=llama.cpp Server
After=network.target

[Service]
Type=simple
WorkingDirectory=/home/linux/llama.cpp/build
ExecStart=/home/linux/llama.cpp/build/bin/llama-server -m /home/linux/models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf -c 4096 -ngl 0 --host 0.0.0.0 --port 8080
Restart=always
User=linux

[Install]
WantedBy=multi-user.target
bash
# 서비스 등록 및 시작
sudo systemctl daemon-reload
sudo systemctl enable llama-server
sudo systemctl start llama-server
sudo systemctl status llama-server

 


7. Qwen 3 6B 모델 다운로드

7.1 Hugging Face CLI 설치

bash
# 1. pipx 설치
apt update && apt install -y pipx
pipx ensurepath

# 2. pipx로 huggingface-hub 설치
pipx install huggingface-hub

# 설치 확인
hf --version

7.2 Qwen 3 8B GGUF 모델 다운로드

bash
# 모델 저장 디렉토리 생성
mkdir -p ~/models

# huggingface 로그인
# 사전 준지
# ㅁ Hugging Face 접속 토큰 준비
# 1. Hugging Face 웹사이트 접속: https://huggingface.co/
# 2. 로그인 (계정이 없으면 회원가입)
# 3. Access Token 생성:
#   - 우측 상단 프로필 사진 클릭 → Settings
#   - 왼쪽 메뉴에서 Access Tokens 클릭
#   - New token 버튼 클릭
#   - Token 이름 입력 (예: my-rag-app)
#   - 권한 선택: Read (다운로드만 할 경우) 또는 Write (업로드도 할 경우)
#   - Generate token 클릭
#   - 생성된 토큰 복사 (형식: hf_xxxxxxxxxxxxxxxxxxxxxxxx)
#
# ※ 기타 방법
# 1. 한 번에 토큰 입력하여 로그인방법
#    hf auth login --token hf_xxxxxxxxxxxxxxxxxxxxxxxx
# 2. 환경 변수로 토큰 설정(자동 로그인)
#    가. 현재 세션만
#      export HF_TOKEN="hf_xxxxxxxxxxxxxxxxxxxxxxxx"
#    나. 영구 설정 (~/.bashrc에 추가)
#      echo 'export HF_TOKEN="hf_xxxxxxxxxxxxxxxxxxxxxxxx"' >> ~/.bashrc
#      source ~/.bashrc
#

$ hf auth login
? Enter your access token: hf_xxxxxxxxxxxxxxxxxxxxxxxx  # 여기에 토큰 붙여넣기
✅ Login successful!
Your token has been saved to /root/.cache/huggingface/token

# Qwen3-0.6B-GGUF 모델 다운로드
# https://huggingface.co/Qwen/Qwen3-0.6B
#./llama-cli -hf Qwen/Qwen3-0.6B-GGUF:Q8_0 --jinja --color -ngl 99 -fa -sm row --temp 0.6 --top-k 20 --top-p 0.95 --min-p 0 --presence-penalty 1.5 -c 40960 -n 32768 --no-context-shift --local-dir /home/linux/projects/my-rag-app/models/
hf download Qwen/Qwen3-0.6B-GGUF --local-dir /home/linux/projects/my-rag-app/models/

# Qwen 3 8B GGUF 모델 다운로드
#hf download bartowski/Qwen_Qwen3.5-2B-GGUF Qwen_Qwen3.5-2B-Q8_0.gguf --local-dir ~/models/qwen3.5-2b

⚠️ 참고: Qwen 3 8B가 없을 경우 대안 모델:

bash
# 대안 1: Mistral 7B (성능 우수)
hf download \
  TheBloke/Mistral-7B-Instruct-v0.3-GGUF \
  mistral-7b-instruct-v0.3.Q4_K_M.gguf \
  --local-dir ~/models/mistral-7b

# 대안 2: Llama 3.1 8B
hf download \
  QuantFactory/Meta-Llama-3.1-8B-Instruct-GGUF \
  Meta-Llama-3.1-8B-Instruct.Q4_K_M.gguf \
  --local-dir ~/models/llama3.1-8b

[참고] 폐쇄망에서 모델 전송 방법

인터넷이 되는 PC에서 USB로 전송:

bash
# 인터넷 PC에서 다운로드 후 USB에 복사
cp -r ~/models /media/usb/

# 폐쇄망 WSL2에서 USB 마운트 (Windows의 D: 드라이브 예시)
sudo mkdir /mnt/d
sudo mount -t drvfs D: /mnt/d
cp -r /mnt/d/models ~/

 

6. Docker 및 PostgreSQL 설정

[참고] 임베딩 서버 ( docker-compose.yml에 정의함)

RAG 시스템을 완전히 오프라인으로 구성하려면 임베딩 서버도 로컬에 설치해야 합니다.

bash
# 텍스트 임베딩 서버 (Docker)
docker run -d \
  --name embedding-server \
  -p 8081:80 \
  -v ~/embeddings_cache:/data \
  -e MODEL_ID=sentence-transformers/all-MiniLM-L6-v2 \
  ghcr.io/huggingface/text-embeddings-inference:cpu-1.2

 

6.1 Docker Compose 파일 수정

docker-compose.yml 파일 수정 (Windows에서 접근 가능하도록):

yaml
  1 services:
  2   # 1. RAG Web App
  3   web:
  4     build:
  5       context: .
  6       dockerfile: Dockerfile
  7     container_name: rag_app
  8     restart: always
  9     ports:
 10       - "3000:3000"
 11     environment:
 12       - DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
 13       - EMBEDDING_URL=http://embedding:8070
 14       - LLM_URL=http://llama:8079
 15       - LEPTOS_SITE_ADDR=0.0.0.0:3000
 16     depends_on:
 17       - db
 18       - embedding
 19       - llama
 20     networks:
 21       - rag-network
 22
 23   # 2. Vector DB (PostgreSQL + pgvector)
 24   db:
 25     image: pgvector/pgvector:pg16
 26     container_name: rag_postgres
 27     restart: always
 28     environment:
 29       POSTGRES_USER: postgres
 30       POSTGRES_PASSWORD: postgres
 31       POSTGRES_DB: ragdb
 32     ports:
 33       - "5432:5432"
 34     volumes:
 35       - pgdata:/var/lib/postgresql/data
 36       - ./init.sql:/docker-entrypoint-initdb.d/init.sql
 37     networks:
 38       - rag-network
 39
 40   # 3. Embedding Server
 41   embedding:
 42     image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5
 43     container_name: embedding_server
 44     restart: always
 45     ports:
 46       - "8070:8070"
 47     environment:
 48       - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
 49     command:
 50       - --model-id
 51       - sentence-transformers/all-MiniLM-L6-v2
 52       - --port
 53       - "8070"
 54       - --hostname
 55       - 0.0.0.0
 56     volumes:
 57       - ./embeddings_cache:/data
 58     networks:
 59       - rag-network
 60
 61   # 4. Local LLM Server (llama.cpp)
 62   llama:
 63     image: ghcr.io/ggml-org/llama.cpp:server
 64     container_name: llama_server
 65     restart: always
 66     ports:
 67       - "8079:8079"
 68     volumes:
 69       - /home/linux/projects/my-rag-app/models:/models
 70     command:
 71       - -m
 72       - /models/Qwen_Qwen3.5-2B-Q8_0.gguf
 73       - -c
 74       - "4096"
 75       - --host
 76       - 0.0.0.0
 77       - --port
 78       - "8079"
 79     deploy:
 80       resources:
 81         limits:
 82           memory: 8G
 83     networks:
 84       - rag-network
 85
 86 networks:
 87   rag-network:
 88     driver: bridge
 89
 90 volumes:
 91   pgdata:

6.2 PostgreSQL + pgvector (Docker)

bash
# 프로젝트 디렉토리로 이동
cd ~/my-rag-app

# PostgreSQL 컨테이너 실행
docker compose up -d 

# 실행 상태 확인
docker compose ps
docker compose logs 

# DB 접속 테스트
docker exec -it rag_postgres psql -U postgres -d ragdb

 


7. Rust/Leptos 애플리케이션 수정

7.1 .env 파일 생성

WSL2 Ubuntu에서 .env 파일 생성:

bash
cd ~/my-rag-app
nano .env
env
# PostgreSQL 연결 정보
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ragdb

# 로컬 LLM 서버 URL (Windows에서 WSL2 접근)
LLM_SERVER_URL=http://localhost:8080
EMBED_SERVER_URL=http://localhost:8081

# Rust 환경 설정
RUST_LOG=info

7.2 app.rs 수정

src/app.rs 파일을 이전 가이드의 수정된 버전으로 교체합니다. (로컬 LLM 서버 연동 코드)

7.3 Cargo.toml 의존성 확인

Cargo.toml에 필요한 의존성이 모두 포함되어 있는지 확인합니다.


8. 애플리케이션 빌드 및 실행

8.1 프로젝트 설정

bash
# 프로젝트 디렉토리로 이동
cd ~/my-rag-app

# 의존성 다운로드 (인터넷 필요)
cargo fetch

# 빌드
cargo build --release

8.2 Leptos 애플리케이션 빌드

bash
# Leptos 도구 설치
cargo install cargo-leptos

# Leptos 빌드 (SSR + Hydrate)
cargo leptos build --release

8.3 서버 실행

bash
# 개발 모드 실행
cargo leptos watch

# 또는 프로덕션 모드로 실행
cargo leptos serve --release

8.4 Windows에서 접속

Windows 브라우저에서 http://localhost:3000 접속

8.5 Docker로 전체 서비스 실행

bash
# Docker Compose로 모든 서비스 실행
docker compose up -d

# 상태 확인
docker compose ps

9. 문제 해결 가이드

9.1 WSL2 네트워크 문제

Windows에서 WSL2 서비스에 접근 불가:

bash
# WSL2 IP 확인
ip addr show eth0 | grep inet

# Windows 방화벽에서 포트 허용
# Windows PowerShell (관리자)
New-NetFirewallRule -DisplayName "Allow 3000" -Direction Inbound -Protocol TCP -LocalPort 3000 -Action Allow

9.2 llama.cpp 서버 오류

메모리 부족:

bash
# 더 작은 모델 사용 (Q2_K)
./llama-server -m ~/models/qwen3-8b/qwen2.5-7b-instruct-q2_k.gguf -c 2048

# 컨텍스트 크기 줄이기
./llama-server -m ~/models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf -c 2048

9.3 Docker 연결 오류

bash
# Docker 데몬 시작
sudo service docker start

# 권한 문제 해결
sudo chmod 666 /var/run/docker.sock

9.4 PostgreSQL 연결 오류

bash
# 컨테이너 상태 확인
docker compose ps

# 로그 확인
docker compose logs db

# DB 재시작
docker compose restart db

9.5 Rust 빌드 오류

bash
# 캐시 정리
cargo clean

# 의존성 재설치
cargo update

# 오프라인 빌드 문제 시
rm Cargo.lock
cargo build --release

9.6 Windows에서 WSL2로 파일 전송

bash
# Windows 파일을 WSL2로 복사
cp /mnt/c/Users/username/Downloads/model.gguf ~/models/

# WSL2 파일을 Windows로 복사
cp ~/my-rag-app/target/release/my-rag-app /mnt/c/Users/username/Desktop/

9.7 Windows에서 WSL2로 접속 실패 

이미지를 보면 Leptos 웹 앱이 성공적으로 빌드되어 [http://0.0.0.0:3000](http://0.0.0.0:3000) 주소로 서버 실행 중(Serving at [http://0.0.0.0:3000](http://0.0.0.0:3000))인 상태입니다.

WSL(Windows Subsystem for Linux) 내에서 실행 중인 이 웹 서버에 접속하는 방법은 접속하려는 위치(Windows 호스트 or 외부 기기)에 따라 달라집니다.

1. Windows 웹 브라우저에서 접속 (가장 일반적인 경우)

WSL2는 Windows 호스트의 localhost port forwarding을 기본적으로 지원합니다.

  • 접속 주소:또는
  • Plaintext
     
    http://127.0.0.1:3000
    
  • Plaintext
     
    http://localhost:3000
    

2. WSL 내부 터미널(CLI)에서 접속 확인

WSL 내에서 서버가 응답하는지 확인하고 싶다면 아래 명령어를 사용합니다.

Bash
 
curl http://localhost:3000

3. Windows localhost 접속이 안 될 때 (IP 직접 접속)

Windows의 Auto-Localhost 포트 포워딩이 제대로 동작하지 않을 때는 WSL의 실제 IP 주소로 직접 접속합니다.

  1. WSL 터미널에서 IP 확인:(예: 172.25.160.1 같은 IP가 출력됩니다.)
  2. Bash
     
    hostname -I
    
  3. Windows 브라우저 주소창에 입력:
  4. Plaintext
     
    http://<WSL_IP_주소>:3000
    

4. 동일 네트워크의 다른 기기(모바일/스마트폰 등)에서 접속

외부 기기에서 Windows PC를 거쳐 WSL 내부 서버로 접속하려면 포트 포워딩 설정이 필요합니다.

  1. Windows PowerShell (관리자 권한) 실행
  2. WSL IP 확인 후 포트 포워딩 규칙 추가:
  3. PowerShell
     
    # <WSL_IP>에 'hostname -I'로 얻은 IP 입력
    netsh interface portproxy add v4tov4 listenport=3000 listenaddress=0.0.0.0 connectport=3000 connectaddress=<WSL_IP>
    
  4. Windows 방화벽 3000번 포트 허용
  5. 외부 기기 브라우저에서 Windows PC의 IP로 접속: http://<Windows_IP>:3000

Tip (Leptos / Rust 관련) 서버가 0.0.0.0:3000 바인딩 상태이므로, 보통은 1번(http://localhost:3000) 방법으로 바로 접속됩니다. 브라우저 주소창에 입력해 보세요!

9.8 이미 사용 중

로그의 핵심 원인은 3000번 포트가 이미 사용 중(address already in use)이라 Docker 컨테이너(rag_app)가 시작되지 못한 것입니다.

WSL 백그라운드나 호스트 시스템에서 이전에 실행한 Leptos 프로세스(cargo leptos watch 등) 또는 기존에 떠 있는 다른 웹 서버가 3000번 포트를 점유하고 있기 때문에 발생합니다.

해결 절차

1단계: 3000번 포트를 점유 중인 프로세스 종료

WSL 터미널에서 아래 명령어를 실행하여 3000번 포트를 사용하는 프로세스를 찾아 종료합니다.

Bash
 
# 1. 3000번 포트를 사용 중인 PID(프로세스 ID) 확인
sudo lsof -i :3000

# 또는 fuser로 3000번 포트를 점유 중인 프로세스 즉시 강제 종료 (가장 간단)
sudo fuser -k 3000/tcp

참고: fuser 명령어가 없다면 sudo apt install psmisc로 설치하거나, lsof에서 확인된 PID를 sudo kill -9 <PID>로 직접 종료해 주세요.

2단계: 기존 Docker 컨테이너 정리

남아있는 Docker 컨테이너 상태를 정리합니다.

Bash
 
docker-compose down

3단계: Docker Compose 재구동

포트 점유가 해제된 후 다시 컨테이너를 올립니다. 이미지 빌드는 이미 성공했으므로 --build 옵션 없이 빠르게 구동할 수 있습니다.

Bash
 
docker-compose up -d

정상 구동 확인

컨테이너들이 오류 없이 정상 실행 중인지 확인합니다.

Bash
 
docker-compose ps

모든 컨테이너(rag_app, rag_postgres, embedding_server, llama_server)의 STATUS가 Up으로 표시되면 완료입니다.

💡 원인 예방 팁

이전 단계에서 안내해 드린 cargo leptos watch를 터미널에서 따로 실행 중이셨다면, Docker 컨테이너로 앱을 띄울 때는 로컬 cargo leptos watch를 Ctrl + C로 먼저 종료해야 3000번 포트 충돌이 일어나지 않습니다.

 

9.7 LLM 접속 실패 

서버에 연결할 수 없다는 오류가 발생했습니다. docker-compose.yml 파일을 보면 llama.cpp 서버가 8080 포트로 매핑되어 있지만, 실제로 컨테이너가 실행되지 않았거나 포트 바인딩에 문제가 있습니다.

단계별 해결 방법

1️⃣ Docker 컨테이너 상태 확인

bash
docker ps -a

모든 컨테이너(rag_app, rag_postgres, embedding_server, llama_server)가 실행 중인지 확인하세요.

2️⃣ llama_server 컨테이너 로그 확인

bash
docker logs llama_server

에러 메시지가 출력된다면 그 내용을 알려주세요.

3️⃣ Docker Compose 서비스 재시작

bash
docker compose down
docker compose up -d

4️⃣ 포트 충돌 확인

다른 프로세스가 8080 포트를 사용 중인지 확인하세요.

bash
sudo lsof -i :8080
# 또는
sudo netstat -tulpn | grep 8080

5️⃣ 모델 파일 경로 확인

docker-compose.yml에서 모델 경로를 확인하세요:

yaml
volumes:
  - /home/linux/models:/models
command:
  - -m
  - /models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf

모델 파일이 실제로 존재하는지 확인:

bash
ls -la /home/linux/models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf

파일이 없으면 모델을 다운로드해야 합니다.

9.7 LLM 접속 실패

LLM 모델 기동 확인 및 CLI 접속/질의 테스트

제공해주신 터미널/브라우저 스크린샷과 소스 코드를 바탕으로 질문하신 3가지 문제의 원인과 해결 방법을 정리해 드립니다.

1. LLM 모델 기동 확인 및 CLI 접속/질의 테스트

🔴 문제 원인

첫 번째 스크린샷의 docker ps 결과를 보면 llama_server 컨테이너 상태가 Restarting (1) 3 seconds ago로 무한 재시작 중입니다. 컨테이너가 정상적으로 떠 있지 않기 때문에 LLM에 질의를 보낼 수 없는 상태입니다.

주요 발생 원인:

  1. 모델 파일 경로 불일치: docker-compose.yml에는 /models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf로 경로가 지정되어 있으나, 실제 디렉토리 구조에는 /models/Qwen_Qwen3.5-2B-Q8_0.gguf로 바로 존재합니다.
  2. 메모리 부족: docker-compose.yml에 메모리 제한이 8G로 잡혀 있으나, WSL2/Docker Desktop의 할당 메모리가 부족하면 llama.cpp 서버 실행 시 OOM(Out of Memory)으로 종료됩니다.

🛠️ 해결 방법

Step 1. docker-compose.yml 경로 수정

llama 서비스의 command 영역에서 모델 파일 경로를 실제 파일 위치로 맞춰줍니다.

YAML
 
  # 4. Local LLM Server (llama.cpp)
  llama:
    image: ghcr.io/ggml-org/llama.cpp:server
    container_name: llama_server
    restart: always
    ports:
      - "8079:8079"
    volumes:
      - /home/linux/projects/my-rag-app/models:/models # [확인] 실제 models 폴더 절대경로
    command:
      - -m
      - /models/Qwen_Qwen3.5-2B-Q8_0.gguf             # [수정] qwen3-8b/ 경로 제거
      - -c
      - "4096"
      - --host
      - 0.0.0.0
      - --port
      - "8079"

수정 후 컨테이너를 재시작합니다:

Bash
 
docker compose up -d llama

로그를 통해 정상 기동을 확인합니다:

Bash
 
docker logs -f llama_server
# "HTTP server listening" 메시지가 뜨면 정상 기동 완료

Step 2. CLI에서 curl로 LLM 서버에 직접 질의 테스트

서버가 정상적으로 떴다면 WSL 터미널에서 OpenAPI 스펙의 Chat Completions API로 테스트할 수 있습니다.

Bash
 
curl http://localhost:8079/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      {"role": "system", "content": "You are a helpful assistant."},
      {"role": "user", "content": "1+1=?"}
    ],
    "temperature": 0.7
  }'

2. Leptos 화면에서 "1+1=?" 입력 시 응답이 없는 이유

🔴 문제 원인

Leptos의 ask_rag 서버 함수는 질문이 들어왔을 때 아래의 순서(파이프라인)로 동작합니다:

  1. PostgreSQL DB 연결
    ->
  2. embedding_server (8070 포트)로 질문 임베딩 생성
    ->
  3. rag_postgres DB에서 유사 문서 벡터 검색
    ->
  4. llama_server (8079 포트)로 최종 답변 요청

1번 문제에서 본 것처럼 llama_server 컨테이너가 다운(Restarting)된 상태이므로, Leptos 백엔드가 LLM 서버에 HTTP 요청을 보낼 때 connection refused 또는 타임아웃 오류가 발생하여 화면 응답이 멈추게 됩니다.

또한, init.sql에 기본 지식 데이터(문서)가 입력되어 있지 않으면 DB 벡터 검색 결과가 비어 있게 됩니다.

🛠️ 해결 방법

  1. 1번 항목의 해결 방법을 적용하여 llama_server를 정상 실행시킵니다.
  2. rag_app 백엔드 로그를 실시간으로 확인하며 어디서 막히는지 점검합니다:
  3.  
  4. Bash
     
    docker logs -f rag_app
    
  5. DB에 최소 1개 이상의 샘플 데이터를 넣어둡니다:
  6. Bash
     
    docker exec -it rag_postgres psql -U postgres -d ragdb -c "
    INSERT INTO documents (content, embedding) 
    VALUES ('1+1은 2입니다.', array_fill(0::real, ARRAY[384])::vector);
    "

3. Leptos 화면에서 같은 UI가 두 번 나오는 이유

🔴 문제 원인

src/app.rs 소스 코드를 보면, 페이지의 전체 HTML 구조를 잡아주는 shell 함수 내부에서 <App/>을 출력하고 있고, 동시에 App 컴포넌트 내부의 view 바인딩이 중복으로 랜더링되면서 하이드레이션(Hydration) 타이밍에 화면이 2번 겹쳐 나오는 현상입니다.

lib.rs를 보면 hydrate 함수에서 mount_to_body(App)을 호출하여 HTML <body> 전체를 <App/>으로 덮어씌우려고 시도합니다.

Rust
 
// lib.rs
#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
    use crate::app::*;
    console_error_panic_hook::set_once();
    leptos::mount::mount_to_body(App); // <-- body 전체를 다시 렌더링
}

서버에서 SSR로 이미 shell을 통해 <App/>이 렌더링된 HTML을 내려줬는데, 클라이언트 측 JS(WASM)가 실행되면서 mount_to_body(App)에 의해 기존 <body>에 UI를 또 하나 덧붙이거나 하이드레이션이 어긋나 두 번 보이게 됩니다.

🛠️ 해결 방법

Leptos 0.7 표준 하이드레이션 방식인 hydrate_body 방식으로 lib.rs 및 app.rs를 정돈해 줍니다.

1) src/lib.rs 수정

mount_to_body 대신 hydrate_body를 사용합니다.

Rust
 
// src/lib.rs
pub mod app;

#[cfg(feature = "hydrate")]
use wasm_bindgen::prelude::*;

#[cfg(feature = "hydrate")]
#[wasm_bindgen]
pub fn hydrate() {
    use crate::app::*;
    console_error_panic_hook::set_once();
    // mount_to_body 대신 hydrate_body 사용 (SSR 결과물과 클라이언트 랜더링 동기화)
    leptos::mount::hydrate_body(App);
}

2) src/app.rs 수정

shell 함수 내부에서 <body> 태그 구조를 점검합니다.

Rust
 
// src/app.rs
use leptos::prelude::*;

pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="ko">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options=options />
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}

수정 후 docker compose up --build -d로 다시 빌드해 주시면 두 번 렌더링되는 현상이 깔끔하게 해결됩니다!

9.8 임베딩 서버 통신 에러

화면에 출력된 오류 메시지를 통해 원인이 정확히 밝혀졌습니다!

오류: error running server function: 임베딩 서버 통신 에러: error sending request for url (http://embedding:8070/embed)

🔴 원인 분석

rag_app 컨테이너가 http://embedding:8070/embed 주소로 임베딩 요청을 보냈으나, embedding_server 컨테이너에 접근할 수 없어서 접속 거부/타임아웃이 발생한 것입니다.

주요 원인은 다음 2가지 중 하나입니다:

  1. embedding_server 컨테이너가 정상적으로 실행되지 않음/다운됨 (모델 다운로드 중이거나 OOM 오류)
  2. 이전 docker-compose.yml 포트 수정 후 컨테이너 네트워크가 깔끔하게 재갱신되지 않음

🛠️ 단계별 해결 방법

Step 1. embedding_server 상태 및 로그 확인

WSL2 터미널에서 임베딩 컨테이너의 상태와 로그를 확인해 보세요.

Bash
 
# 1. 컨테이너 상태 확인
docker ps -a | grep embedding

# 2. 임베딩 서버 실행 로그 확인
docker logs -f embedding_server
  • 체크 포인트:
    • 로그에 sentence-transformers/all-MiniLM-L6-v2 모델을 다운로드 중(Downloading...)이라면, 모델 다운로드가 완전히 끝날 때까지 기다려야 합니다.
    • 만약 다운로드 완료 후 Ready 또는 HTTP server listening 상태가 나오지 않고 종료되었다면, 메모리 부족(OOM) 문제일 수 있습니다.

Step 2. 임베딩 서버에 직접 핑(Ping) / API 요청 테스트

rag_app 대신 백엔드에서 직접 임베딩 서버 API가 살아있는지 테스트해 봅니다.

Bash
 
curl http://localhost:8070/embed \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"inputs": "test"}'
  • 정상 동작 시: [[0.012, -0.045, ...]] 형태의 숫자 배열(Vector) 데이터가 반환됩니다.
  • 연결 거부 시: 컨테이너를 재시작해 주어야 합니다.

Step 3. 전체 컨테이너 네트워크 재재기동

포트 설정을 변경했으므로, 기존 네트워크 연결이 꼬였을 수 있습니다. 아래 명령어로 전체 서비스를 깔끔하게 재시작해 주세요.

Bash
 
# 컨테이너 및 내부 브리지 네트워크 완전히 제거
docker compose down -v

# 컨테이너 다시 실행 (이미 빌드되어 있으므로 빠르게 올라옵니다)
docker compose up -d

Step 4. 모든 컨테이너 정상 가동 확인

Bash
 
docker ps

아래와 같이 모든 주요 컨테이너의 STATUS가 Up X minutes 상태여야 합니다 (Restarting 또는 Exited가 없어야 합니다).

  • rag_postgres (5432 포트)
  • embedding_server (8070 포트)
  • llama_server (8079 포트)
  • rag_app (3000 포트)

모든 컨테이너가 Up 상태인 것을 확인한 후 다시 브라우저(http://localhost:3000)에서 "1+1=?" 질문을 입력하시면 정상적으로 응답을 받으실 수 있습니다!


완전한 초기화 및 재시작

문제가 지속되면 다음 단계를 수행하세요:

1. 모든 컨테이너 중지 및 제거

bash
docker compose down -v

2. 도커 볼륨도 제거 (데이터 초기화)

bash
docker volume prune -f

3. 모델 파일 확인/다운로드

모델이 없다면 Hugging Face에서 다운로드하세요:

bash
# models 디렉토리 생성
mkdir -p /home/linux/models/qwen3-8b

# 모델 다운로드 (예시)
wget -O /home/linux/models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf \
  https://huggingface.co/ggml-org/Qwen3.5-2B-GGUF/resolve/main/Qwen3.5-2B-Q8_0.gguf

4. 컨테이너 다시 실행

bash
docker compose up -d

5. 실행 상태 확인

bash
docker compose ps
docker logs llama_server

6. 서버 응답 테스트

bash
# 잠시 기다린 후 (모델 로딩 시간 필요)
sleep 10
curl http://localhost:8080/health

 


대체 방법: llama.cpp 직접 실행

Docker 대신 직접 실행해 볼 수도 있습니다:

bash
# llama.cpp 빌드 (이미 빌드되어 있다고 가정)
./llama-server -m /home/linux/models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf \
  -c 4096 \
  --host 0.0.0.0 \
  --port 8080

 

9.8 model과 llm 연결 오류

model 컨테이너가 보이지 않는다. 원래 안보이는게 맞나?, model container up 되지 않나?

, 보이지 않는 것이 지극히 정상이며 올바르게 설정된 상태입니다!

model이나 .gguf 파일은 그 자체로 실행되는 컨테이너가 아니라, llama_server 컨테이너가 읽어서 실행하는 '데이터 파일(가중치 파일)'이기 때문입니다.

YML

💡 컨테이너와 모델 파일의 관계

현재 작성하신 docker-compose.yml 구조를 보면 역할이 깔끔하게 분리되어 있습니다.

YML

구분 이름 설명
프로그램 (컨테이너) llama_server AI 모델을 구동하고 HTTP API 서버를 띄워주는 실행 엔진 (ghcr.io/ggml-org/llama.cpp:server)
YML
데이터 (파일) Qwen_Qwen3.5-2B-Q8_0.gguf 호스트 디스크 (/models 폴더)에 존재하는 LLM 모델 파일
YML

llama_server 컨테이너가 켜질 때, 볼륨 마운트(volumes: - /home/linux/...:/models)를 통해 호스트 PC에 있는 .gguf 모델 파일을 불러와 메모리에 올려 구동하는 방식입니다.

YML

🔍 llama_server가 모델을 정상적으로 로드했는지 확인하는 방법

models가 컨테이너로 뜨지는 않지만, llama_server가 모델 파일(Qwen_...gguf)을 정상적으로 읽어서 켰는지 로그로 바로 확인하실 수 있습니다.

YML

터미널에 아래 명령어를 입력해 보세요:

Bash

docker logs llama_server

정상 동작 시 로그 화면 예시

로그 하단에 다음과 비슷한 문구가 뜨면 모델 파일이 메모리에 정상적으로 올라간 것입니다.

Plaintext

llama_model_loader: loaded meta data with 23 key-value pairs...

llama_init_from_model: loaded model

HTTP server listening at http://0.0.0.0:8079

이미지에서 모든 컨테이너(rag_postgres, llama_server, embedding_server, rag_app)Started 상태로 초록색 불이 들어온 것을 보니 이제 완벽하게 기동된 상태입니다!

이제 웹 브라우저(http://localhost:3000)로 접속하셔서 질의를 테스트해 보시면 됩니다.

Want to test if the model produces an answer using a cURL command?

해결 방법?

curl: (56) Recv failure: Connection reset by peer 오류는 임베딩 컨테이너(embedding_server)가 요청을 받는 즉시 강제로 연결을 끊어버렸을 때 발생합니다.

주요 원인은 임베딩 모델(sentence-transformers/all-MiniLM-L6-v2)을 다운로드 중이거나, 실행 중 에러(메모리 부족 또는 권한/네트워크 이슈)로 인해 컨테이너 프로세스가 다운되었기 때문입니다.

YML

🛠단계별 해결 방법

Step 1. 컨테이너 내부 실행 로그 확인 (가장 중요)

컨테이너가 왜 연결을 끊었는지 원인을 파악하기 위해 아래 명령어로 embedding_server 로그를 확인해 주세요.

Bash

docker logs --tail 50 embedding_server

경우 A (모델 다운로드 중): 로그 끝부분에 Downloading... 이나 Fetching 8 files... 같은 다운로드 진행 상황이 보인다면, 다운로드가 완료될 때까지 잠시 기다려야 합니다.

경우 B (메모리 부족 또는 내부 에러): Killed 또는 Out of memory / Error: ... 문구가 출력되며 튕겼다면 모델을 불러오는 도중 컨테이너가 꺼진 것입니다.

Step 2. 임베딩 서버 모델 캐시 권한 및 디렉토리 확인

docker-compose.yml 설정상 호스트의 ./embeddings_cache 폴더를 컨테이너의 /data 경로로 바인드 마운트하고 있습니다. 폴더 생성 권한이 꼬였을 수 있으므로 해당 디렉토리를 정리 후 재시작합니다.

YML

Bash

# 1. 프로젝트 경로로 이동

cd /home/linux/projects/my-rag-app

 

# 2. 기존 임베딩 캐시 권한 부여 및 정리

sudo chmod -R 777 ./embeddings_cache

 

# 3. 임베딩 컨테이너만 재시작

docker compose restart embedding

Step 3. TEI(Text Embeddings Inference) 엔드포인트 테스트

huggingface/text-embeddings-inference 이미지의 기본 테스트 경로인 / (Health check) 또는 /embed로 다시 요청해 봅니다.

1) Health Check 확인:

Bash

curl http://localhost:8070/health

OK 또는 200 응답이 나오면 서버 준비 완료 상태입니다.

2) 임베딩 벡터 생성 테스트:

Bash

curl http://localhost:8070/embed \

  -X POST \

  -H "Content-Type: application/json" \

  -d '{"inputs": "test"}'

Step 4. 대체 이미지 변경 (최후의 방법)

만약 CPU 전용 이미지인 cpu-1.2에서 지속적으로 Connection reset이 일어난다면, docker-compose.yml의 임베딩 서버 이미지 버전을 최신 안정판으로 변경해 봅니다.

YML

docker-compose.yml 수정:

YML

YAML

  # 3. Embedding Server

  embedding:

    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5  # <--- cpu-1.2에서 cpu-1.5로 변경 권장

    container_name: embedding_server

    restart: always

    ports:

      - "8070:8070"

    environment:

      - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2

    volumes:

      - ./embeddings_cache:/data

    networks:

      - rag-network

변경 후 재시작:

Bash

docker compose up -d embedding

로그(docker logs --tail 50 embedding_server)를 확인해 보시고, 출력되는 에러 문구가 있다면 알려주시면 더 정확한 해결책을 바로 제공해 드리겠습니다!

안되는데 해결방법은?

text-embeddings-inference (TEI) 컨테이너가 실행 직후 계속 죽거나(Crash) 접속을 거부하는 대표적인 원인은 크게 2가지입니다.

CPU 명령어 세트 미지원: TEI 컨테이너의 최신 버전은 CPUAVX2 / FMA 명령어를 요구하는데, WSL2 환경이나 구형 CPU에서 이를 지원하지 않아 시작하자마자 Crash됩니다.

--port 인자 누락: TEI 이미지 내부의 기본 포트는 80인데, docker-compose.yml에서 실행 명령(command)--port 8070을 명시해 주지 않아서 포트 매핑 불일치가 발생하는 경우입니다.

YML

이 문제는 가장 안정적이고 가벼운 Python 기반 임베딩 서버(FastAPI + sentence-transformers)로 교체하거나 TEI 컨테이너 옵션을 조정하면 깔끔하게 해결됩니다.

🛠해결 방법 (2가지 중 선택)

방법 A. 가장 확실한 해결법 (Python 기반 FastEmbed/Sentence-Transformers로 변경)

WSL2 환경에서 TEI 컨테이너의 CPU AVX 호환성 에러를 완벽히 회피하는 가장 안정적인 방법입니다.

1. docker-compose.ymlembedding 서비스 수정

기존 embedding 서비스 부분을 아래와 같이 ghcr.io/huggingface/text-embeddings-inference 대신 Python 기반의 호환성 높은 임베딩 이미지 또는 간단한 파이썬 서버로 교체하거나, ollama를 임베딩 전용으로 띄우는 것이 좋습니다.

만약 TEI를 그대로 유지하고 싶다면 방법 B를 먼저 시도해 보세요.

방법 B. TEI 컨테이너 설정 보완 (포트 및 실행 명령어 명시)

docker-compose.yml 파일에서 embedding 서비스에 --port 8070 및 호스트 바인드 옵션을 명시합니다.

YML

1. docker-compose.yml 수정

YAML

  # 3. Embedding Server

  embedding:

    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5

    container_name: embedding_server

    restart: always

    ports:

      - "8070:8070"

    environment:

      - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2

      - PORT=8070

    command:

      - --model-id

      - sentence-transformers/all-MiniLM-L6-v2

      - --port

      - "8070"

      - --hostname

      - 0.0.0.0

    volumes:

      - ./embeddings_cache:/data

    networks:

      - rag-network

2. 컨테이너 재실행 및 로그 점검

Bash

# 1. 컨테이너 재시작

docker compose up -d embedding

 

# 2. 실행 로그 즉시 확인 (원인 파악용)

docker logs -f embedding_server

🔍 로그에 따른 원인별 조치법

docker logs -f embedding_server 실행 시 출력되는 문구로 최종 확인이 가능합니다:

Illegal instruction (core dumped) 메시지가 보일 경우:

CPU AVX2 명령어를 지원하지 않아 발생하는 문제입니다.

해결책: docker-compose.yml의 이미지를 ghcr.io/huggingface/text-embeddings-inference:cpu-1.2 이하 버전으로 내리거나, 임베딩 전용 라이브러리로 대체해야 합니다.

Downloading... 문구가 출력되는 경우:

모델 다운로드가 아직 끝나지 않은 상태입니다. 로그에 Ready 문구가 뜰 때까지 1~2분 기다린 후 curl http://localhost:8070/health를 재시도하세요.

 


문제가 해결되지 않으면

로그 출력 결과를 공유해 주세요:

bash
docker logs llama_server --tail 50

위 명령어의 출력을 보내주시면 구체적인 문제를 진단해 드리겠습니다.

 

model 컨테이너가 보이지 않는다. 원래 안보이는게 맞나?, model은 container로 up 되지 않나?

 
 
 

네, 보이지 않는 것이 지극히 정상이며 올바르게 설정된 상태입니다!

model이나 .gguf 파일은 그 자체로 실행되는 컨테이너가 아니라, llama_server 컨테이너가 읽어서 실행하는 '데이터 파일(가중치 파일)'이기 때문입니다.

YML

💡 컨테이너와 모델 파일의 관계

현재 작성하신 docker-compose.yml 구조를 보면 역할이 깔끔하게 분리되어 있습니다.

YML
구분이름설명
프로그램 (컨테이너) llama_server AI 모델을 구동하고 HTTP API 서버를 띄워주는 실행 엔진 (ghcr.io/ggml-org/llama.cpp:server)
YML

데이터 (파일) Qwen_Qwen3.5-2B-Q8_0.gguf 호스트 디스크 (/models 폴더)에 존재하는 LLM 모델 파일
YML

llama_server 컨테이너가 켜질 때, 볼륨 마운트(volumes: - /home/linux/...:/models)를 통해 호스트 PC에 있는 .gguf 모델 파일을 불러와 메모리에 올려 구동하는 방식입니다.

YML

🔍 llama_server가 모델을 정상적으로 로드했는지 확인하는 방법

models가 컨테이너로 뜨지는 않지만, llama_server가 모델 파일(Qwen_...gguf)을 정상적으로 읽어서 켰는지 로그로 바로 확인하실 수 있습니다.

YML

터미널에 아래 명령어를 입력해 보세요:

Bash
 
docker logs llama_server

✅ 정상 동작 시 로그 화면 예시

로그 하단에 다음과 비슷한 문구가 뜨면 모델 파일이 메모리에 정상적으로 올라간 것입니다.

Plaintext
 
llama_model_loader: loaded meta data with 23 key-value pairs...
llama_init_from_model: loaded model
HTTP server listening at http://0.0.0.0:8079

이미지에서 모든 컨테이너(rag_postgres, llama_server, embedding_server, rag_app)가 Started 상태로 초록색 불이 들어온 것을 보니 이제 완벽하게 기동된 상태입니다!

이제 웹 브라우저(http://localhost:3000)로 접속하셔서 질의를 테스트해 보시면 됩니다.

Want to test if the model produces an answer using a cURL command?
 
 

해결 방법?

 
 
 

curl: (56) Recv failure: Connection reset by peer 오류는 임베딩 컨테이너(embedding_server)가 요청을 받는 즉시 강제로 연결을 끊어버렸을 때 발생합니다.

주요 원인은 임베딩 모델(sentence-transformers/all-MiniLM-L6-v2)을 다운로드 중이거나, 실행 중 에러(메모리 부족 또는 권한/네트워크 이슈)로 인해 컨테이너 프로세스가 다운되었기 때문입니다.

YML

🛠️ 단계별 해결 방법

Step 1. 컨테이너 내부 실행 로그 확인 (가장 중요)

컨테이너가 왜 연결을 끊었는지 원인을 파악하기 위해 아래 명령어로 embedding_server 로그를 확인해 주세요.

Bash
 
docker logs --tail 50 embedding_server
  • 경우 A (모델 다운로드 중): 로그 끝부분에 Downloading... 이나 Fetching 8 files... 같은 다운로드 진행 상황이 보인다면, 다운로드가 완료될 때까지 잠시 기다려야 합니다.
  • 경우 B (메모리 부족 또는 내부 에러): Killed 또는 Out of memory / Error: ... 문구가 출력되며 튕겼다면 모델을 불러오는 도중 컨테이너가 꺼진 것입니다.

Step 2. 임베딩 서버 모델 캐시 권한 및 디렉토리 확인

docker-compose.yml 설정상 호스트의 ./embeddings_cache 폴더를 컨테이너의 /data 경로로 바인드 마운트하고 있습니다. 폴더 생성 권한이 꼬였을 수 있으므로 해당 디렉토리를 정리 후 재시작합니다.

YML
Bash
 
# 1. 프로젝트 경로로 이동
cd /home/linux/projects/my-rag-app

# 2. 기존 임베딩 캐시 권한 부여 및 정리
sudo chmod -R 777 ./embeddings_cache

# 3. 임베딩 컨테이너만 재시작
docker compose restart embedding

Step 3. TEI(Text Embeddings Inference) 엔드포인트 테스트

huggingface/text-embeddings-inference 이미지의 기본 테스트 경로인 / (Health check) 또는 /embed로 다시 요청해 봅니다.

1) Health Check 확인:

Bash
 
curl http://localhost:8070/health

OK 또는 200 응답이 나오면 서버 준비 완료 상태입니다.

2) 임베딩 벡터 생성 테스트:

Bash
 
curl http://localhost:8070/embed \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"inputs": "test"}'

Step 4. 대체 이미지 변경 (최후의 방법)

만약 CPU 전용 이미지인 cpu-1.2에서 지속적으로 Connection reset이 일어난다면, docker-compose.yml의 임베딩 서버 이미지 버전을 최신 안정판으로 변경해 봅니다.

YML

docker-compose.yml 수정:

YML
YAML
 
  # 3. Embedding Server
  embedding:
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5  # <--- cpu-1.2에서 cpu-1.5로 변경 권장
    container_name: embedding_server
    restart: always
    ports:
      - "8070:8070"
    environment:
      - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
    volumes:
      - ./embeddings_cache:/data
    networks:
      - rag-network

변경 후 재시작:

Bash
 
docker compose up -d embedding

로그(docker logs --tail 50 embedding_server)를 확인해 보시고, 출력되는 에러 문구가 있다면 알려주시면 더 정확한 해결책을 바로 제공해 드리겠습니다!

 
 
 

안되는데 해결방법은?

 
 

text-embeddings-inference (TEI) 컨테이너가 실행 직후 계속 죽거나(Crash) 접속을 거부하는 대표적인 원인은 크게 2가지입니다.

  1. CPU 명령어 세트 미지원: TEI 컨테이너의 최신 버전은 CPU의 AVX2 / FMA 명령어를 요구하는데, WSL2 환경이나 구형 CPU에서 이를 지원하지 않아 시작하자마자 Crash됩니다.
  2. --port 인자 누락: TEI 이미지 내부의 기본 포트는 80인데, docker-compose.yml에서 실행 명령(command)에 --port 8070을 명시해 주지 않아서 포트 매핑 불일치가 발생하는 경우입니다.
  3. YML

이 문제는 가장 안정적이고 가벼운 Python 기반 임베딩 서버(FastAPI + sentence-transformers)로 교체하거나 TEI 컨테이너 옵션을 조정하면 깔끔하게 해결됩니다.

🛠️ 해결 방법 (2가지 중 선택)

방법 A. 가장 확실한 해결법 (Python 기반 FastEmbed/Sentence-Transformers로 변경)

WSL2 환경에서 TEI 컨테이너의 CPU AVX 호환성 에러를 완벽히 회피하는 가장 안정적인 방법입니다.

1. docker-compose.yml의 embedding 서비스 수정

기존 embedding 서비스 부분을 아래와 같이 ghcr.io/huggingface/text-embeddings-inference 대신 Python 기반의 호환성 높은 임베딩 이미지 또는 간단한 파이썬 서버로 교체하거나, ollama를 임베딩 전용으로 띄우는 것이 좋습니다.

만약 TEI를 그대로 유지하고 싶다면 방법 B를 먼저 시도해 보세요.

방법 B. TEI 컨테이너 설정 보완 (포트 및 실행 명령어 명시)

docker-compose.yml 파일에서 embedding 서비스에 --port 8070 및 호스트 바인드 옵션을 명시합니다.

YML

1. docker-compose.yml 수정

YAML
 
  # 3. Embedding Server
  embedding:
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.5
    container_name: embedding_server
    restart: always
    ports:
      - "8070:8070"
    environment:
      - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
      - PORT=8070
    command:
      - --model-id
      - sentence-transformers/all-MiniLM-L6-v2
      - --port
      - "8070"
      - --hostname
      - 0.0.0.0
    volumes:
      - ./embeddings_cache:/data
    networks:
      - rag-network

2. 컨테이너 재실행 및 로그 점검

Bash
 
# 1. 컨테이너 재시작
docker compose up -d embedding

# 2. 실행 로그 즉시 확인 (원인 파악용)
docker logs -f embedding_server

🔍 로그에 따른 원인별 조치법

docker logs -f embedding_server 실행 시 출력되는 문구로 최종 확인이 가능합니다:

  • Illegal instruction (core dumped) 메시지가 보일 경우:
    • CPU의 AVX2 명령어를 지원하지 않아 발생하는 문제입니다.
    • 해결책: docker-compose.yml의 이미지를 ghcr.io/huggingface/text-embeddings-inference:cpu-1.2 이하 버전으로 내리거나, 임베딩 전용 라이브러리로 대체해야 합니다.
  • Downloading... 문구가 출력되는 경우:
    • 모델 다운로드가 아직 끝나지 않은 상태입니다. 로그에 Ready 문구가 뜰 때까지 1~2분 기다린 후 curl http://localhost:8070/health를 재시도하세요.

🔧 성능 최적화 팁

1. Windows 메모리 최적화

powershell
# WSL2 메모리 제한 설정
# C:\Users\username\.wslconfig 파일 생성
[wsl2]
memory=8GB
processors=4

2. llama.cpp 최적화

bash
# CPU 캐시 크기 설정
./llama-server -m model.gguf -c 4096 -t 8 --cache-type-k q4_0

# 배치 처리 크기 증가
./llama-server -m model.gguf -c 4096 -b 512

3. PostgreSQL 최적화

sql
-- pgvector 인덱스 최적화
CREATE INDEX CONCURRENTLY docs_embedding_hnsw_idx 
ON documents USING hnsw (embedding vector_cosine_ops) 
WITH (m = 32, ef_construction = 200);

📝 전체 실행 순서 요약

  1. WSL2 Ubuntu 실행
  2. bash
    wsl -d Ubuntu-24.04
  3. llama.cpp 서버 실행
  4. bash
    cd ~/llama.cpp
    ./llama-server -m ~/models/qwen3-8b/qwen2.5-7b-instruct-q4_k_m.gguf -c 4096 --host 0.0.0.0 --port 8080
  5. Docker 서비스 실행
  6. bash
    cd ~/my-rag-app
    docker compose up -d
  7. Leptos 앱 실행
  8. bash
    cd ~/my-rag-app
    cargo leptos watch
  9. Windows 브라우저에서 접속
  10. text
    http://localhost:3000

📚 수정 내용

  • 2026. 8. 12. 

현재 발생한 OPENAI_API_KEY missing 오류는 src/app.rs의 ask_rag 서버 함수 내부에서 외부 OpenAI API를 호출하도록 작성되어 있고, Environment에서 OPENAI_API_KEY를 필수적으로 참조하려 하기 때문입니다.

docker-compose.yml을 보면 이미 로컬 폐쇄망 환경에서 llama.cpp 서버(http://llama:8080 또는 http://localhost:8080)와 임베딩 서버(http://embedding:8081 또는 http://localhost:8081)가 컨테이너로 구성되어 있습니다.

폐쇄망 환경에 맞춰 외부 API(OpenAI) 호출을 제거하고 내부 로컬 API(llama.cpp 및 TEI)를 바라보도록 소스 코드 및 관련 구성을 수정하는 전체 가이드입니다.

1. src/app.rs 수정

OpenAI API 엔드포인트(api.openai.com) 및 API KEY 체킹 코드를 제거하고, 로컬 LLM 및 Embedding 서버 API 규격에 맞춰 수정합니다.

참고 (차원 수 및 포맷):

  • text-embeddings-inference (all-MiniLM-L6-v2)를 사용하는 경우 출력 차원은 384차원입니다. (init.sql의 DB 차원 설정도 맞춰주어야 합니다.)
  • llama.cpp server는 OpenAI 호환 /v1/chat/completions API를 지원하므로 API Key 없이 바로 호출 가능합니다.
Rust
 
use leptos::prelude::*;

pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="ko">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options=options />
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}

// Leptos Server Function: 질문 시 백엔드에서 벡터 검색 및 로컬 LLM 답변 생성
#[server(AskRag, "/api")]
pub async fn ask_rag(question: String) -> Result<String, ServerFnError> {
    use pgvector::Vector;
    use sqlx::PgPool;
    use std::env;

    dotenvy::dotenv().ok();
    let db_url = env::var("DATABASE_URL").map_err(|_| ServerFnError::new("DATABASE_URL missing"))?;
    
    // 폐쇄망 서비스 엔드포인트 환경변수 (기본값 설정)
    let embed_url = env::var("EMBEDDING_URL").unwrap_or_else(|_| "http://localhost:8081".to_string());
    let llm_url = env::var("LLM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());

    let pool = PgPool::connect(&db_url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
    let client = reqwest::Client::new();

    // 1. 질문을 로컬 TEI(Text Embeddings Inference) 서버를 통해 벡터로 변환 (all-MiniLM-L6-v2: 384차원)
    let embed_res = client
        .post(format!("{}/embed", embed_url))
        .json(&serde_json::json!({
            "inputs": question
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("Embedding server error: {}", e)))?;

    let vec_data: Vec<Vec<f32>> = embed_res.json().await
        .map_err(|e| ServerFnError::new(format!("Embedding parsing error: {}", e)))?;
    
    let query_vec_data = vec_data.first()
        .ok_or_else(|| ServerFnError::new("Empty embedding response"))?;
    let query_vector = Vector::from(query_vec_data.clone());

    // 2. pgvector Cosine similarity 검색
    let vec_str = format!(
        "[{}]",
        query_vector
             .to_vec()
             .iter()
             .map(|f| f.to_string())
             .collect::<Vec<String>>()
             .join(",")
    );

    let docs: Vec<String> = sqlx::query_scalar(
        r#"
        SELECT content
        FROM documents
        ORDER BY embedding <-> $1::vector
        LIMIT 3
        "#
    )
    .bind(vec_str)
    .fetch_all(&pool)
    .await
    .map_err(|e| ServerFnError::new(e.to_string()))?;

    let context = docs.join("\n---\n");

    // 3. Prompt 구성 및 로컬 llama.cpp 서버 (Chat Completions) 호출
    let prompt = format!(
        "다음 문맥을 참조하여 질문에 답변하세요.\n\n[문맥]\n{}\n\n[질문]\n{}",
        if context.is_empty() { "관련 검색 데이터 없음" } else { &context },
        question
    );

    let llm_res = client
        .post(format!("{}/v1/chat/completions", llm_url))
        .json(&serde_json::json!({
            "messages": [
                {"role": "system", "content": "너는 제공된 문맥을 기반으로 정확하게 답변하는 도우미이다."},
                {"role": "user", "content": prompt}
            ],
            "temperature": 0.7
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("LLM server error: {}", e)))?;

    let llm_json: serde_json::Value = llm_res.json().await
        .map_err(|e| ServerFnError::new(format!("LLM response parsing error: {}", e)))?;
        
    let answer = llm_json["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("답변을 생성하지 못했습니다.")
        .to_string();

    Ok(answer)
}

#[component]
pub fn App() -> impl IntoView {
    let (question, set_question) = signal(String::new());
    let ask_action = ServerAction::<AskRag>::new();

    let pending = ask_action.pending();
    let value = ask_action.value();

    let on_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();
        if !question.get().trim().is_empty() {
            ask_action.dispatch(AskRag { question: question.get() });
        }
    };

    view! {
        <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif; padding: 20px;">
            <h2>"Leptos + PostgreSQL + pgvector RAG (Docker)"</h2>
            <form on:submit=on_submit style="display: flex; gap: 10px; margin-bottom: 20px;">
                <input
                    type="text"
                    style="flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"
                    placeholder="질문을 입력하세요..."
                    prop:value=question
                    on:input=move |e| set_question.set(event_target_value(&e))
                />
                <button type="submit" disabled=move || pending.get() style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    {move || if pending.get() { "검색 중..." } else { "질문하기" }}
                </button>
            </form>

            <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #e9ecef; min-height: 100px;">
            {move || match value.get() {
                    None => view! { <p style="color: #6c757d;">{"질문을 입력하세요.".to_string()}</p> }.into_view(),
                    Some(Ok(ans)) => view! { <p style="white-space: pre-wrap;">{ans}</p> }.into_view(),
                    Some(Err(err)) => view! { <p style="color: red;">{format!("오류: {}", err)}</p> }.into_view(),
                }}
            </div>
        </div>
    }
}

2. .env 파일 수정/생성

프로젝트 루트의 .env 파일에서 OPENAI_API_KEY 항목을 삭제하고, 로컬 폐쇄망 내부 컨테이너 주소를 정의합니다.

코드 스니펫
 
DATABASE_URL=postgres://postgres:postgres@localhost:5432/ragdb
EMBEDDING_URL=http://localhost:8081
LLM_URL=http://localhost:8080

(Docker Compose 네트워크 상에서 WASM/Backend 컨테이너가 직접 접근하는 환경이라면 http://embedding:80 및 http://llama:8080 형태로 변경할 수 있습니다.)

3. init.sql 수정 (임베딩 차원 변경)

OpenAI의 text-embedding-3-small은 1536차원이지만, 로컬 all-MiniLM-L6-v2 모델은 384차원입니다. DB 스키마 차원도 맞춰주어야 pgvector 에러가 발생하지 않습니다.

SQL
 
-- pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector; 

-- 문서 저장 테이블 (차원을 1536 -> 384로 수정)
CREATE TABLE IF NOT EXISTS documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    embedding VECTOR(384) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
); 

-- Cosine Distance HNSW 인덱스 생성
CREATE INDEX IF NOT EXISTS docs_embedding_hnsw_idx
ON documents USING hnsw (embedding vector_cosine_ops);

4. docker-compose.yml 서비스 추가 (앱 컨테이너 추가 시)

만약 Leptos 웹 앱 서비스도 Docker Compose로 같이 띄워 폐쇄망 내부 네트워크로 묶으려면 docker-compose.yml 끝에 웹 앱 서비스를 추가해 줍니다.

YAML
 
  web:
    build: .
    container_name: rag_app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
      - EMBEDDING_URL=http://embedding:80
      - LLM_URL=http://llama:8080
    depends_on:
      - db
      - embedding
      - llama

5. 재경량화 및 적용 방법

수정을 완료한 후 기존 PostgreSQL DB 볼륨의 임베딩 차원을 업데이트하고 서버를 재시작합니다.

Bash
 
# 1. 기존 DB 데이터 초기화 (차원이 1536에서 384로 변경되었기 때문)
docker-compose down -v

# 2. 로컬 서비스 및 컨테이너 재시작
docker-compose up -d

# 3. Leptos 앱 다시 빌드 및 실행
cargo leptos watch

 

 


이 가이드를 따라하면 Windows 11 WSL2 환경에서 완전히 로컬에서 동작하는 RAG 시스템을 구축할 수 있습니다!

# 설치 시 장애 해결

프로젝트 경로는 "\\wsl.localhost\Ubuntu-24.04\home\linux\projects\my-rag-app".

로컬에서 "http://172.24.27.59:3000/?" 접속해서 "1+1+?" 검색하면 "오류: error running server function: OPENAI_API_KEY missing" 표시된다.

폐쇄망 환경에서 llm 모델(models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf) vectordb 구성한 시스템이다.

폐쇄망 환경을 고려해서 "OPENAI_API_KEY" 구성을 폐쇄망 환경으로 구성할 있도록 소스(app.rs 관련 소스 전체) 수정을 해줘.

그리고 기타 구성에서 "OPENAI_API_KEY" 있다면 수정해줘 

현재 발생한 OPENAI_API_KEY missing 오류는 src/app.rs ask_rag 서버 함수 내부에서 외부 OpenAI API 호출하도록 작성되어 있고, Environment에서 OPENAI_API_KEY 필수적으로 참조하려 하기 때문입니다.

docker-compose.yml 보면 이미 로컬 폐쇄망 환경에서 llama.cpp 서버(http://llama:8080 또는 http://localhost:8080) 임베딩 서버(http://embedding:8081 또는 http://localhost:8081) 컨테이너로 구성되어 있습니다.

폐쇄망 환경에 맞춰 외부 API(OpenAI) 호출을 제거하고 내부 로컬 API(llama.cpp TEI) 바라보도록 소스 코드 관련 구성을 수정하는 전체 가이드입니다.

1. src/app.rs 수정

OpenAI API 엔드포인트(api.openai.com) API KEY 체킹 코드를 제거하고, 로컬 LLM Embedding 서버 API 규격에 맞춰 수정합니다.

참고 (차원 포맷):

·        text-embeddings-inference (all-MiniLM-L6-v2) 사용하는 경우 출력 차원은 384차원입니다. (init.sql DB 차원 설정도 맞춰주어야 합니다.)

·        llama.cpp server OpenAI 호환 /v1/chat/completions API 지원하므로 API Key 없이 바로 호출 가능합니다.

Rust

use leptos::prelude::*;
 
pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="ko">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options=options />
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}
 
// Leptos Server Function: 질문 시 백엔드에서 벡터 검색 및 로컬 LLM 답변 생성
#[server(AskRag, "/api")]
pub async fn ask_rag(question: String) -> Result<String, ServerFnError> {
    use pgvector::Vector;
    use sqlx::PgPool;
    use std::env;
 
    dotenvy::dotenv().ok();
    let db_url = env::var("DATABASE_URL").map_err(|_| ServerFnError::new("DATABASE_URL missing"))?;
    
    // 폐쇄망 서비스 엔드포인트 환경변수 (기본값 설정)
    let embed_url = env::var("EMBEDDING_URL").unwrap_or_else(|_| "http://localhost:8081".to_string());
    let llm_url = env::var("LLM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());
 
    let pool = PgPool::connect(&db_url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
    let client = reqwest::Client::new();
 
    // 1. 질문을 로컬 TEI(Text Embeddings Inference) 서버를 통해 벡터로 변환 (all-MiniLM-L6-v2: 384차원)
    let embed_res = client
        .post(format!("{}/embed", embed_url))
        .json(&serde_json::json!({
            "inputs": question
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("Embedding server error: {}", e)))?;
 
    let vec_data: Vec<Vec<f32>> = embed_res.json().await
        .map_err(|e| ServerFnError::new(format!("Embedding parsing error: {}", e)))?;
    
    let query_vec_data = vec_data.first()
        .ok_or_else(|| ServerFnError::new("Empty embedding response"))?;
    let query_vector = Vector::from(query_vec_data.clone());
 
    // 2. pgvector Cosine similarity 검색
    let vec_str = format!(
        "[{}]",
        query_vector
             .to_vec()
             .iter()
             .map(|f| f.to_string())
             .collect::<Vec<String>>()
             .join(",")
    );
 
    let docs: Vec<String> = sqlx::query_scalar(
        r#"
        SELECT content
        FROM documents

 

        ORDER BY embedding <-> $1::vector
        LIMIT 3
        "#
    )
    .bind(vec_str)
    .fetch_all(&pool)
    .await
    .map_err(|e| ServerFnError::new(e.to_string()))?;
 
    let context = docs.join("\n---\n");
 
    // 3. Prompt 구성 및 로컬 llama.cpp 서버 (Chat Completions) 호출
    let prompt = format!(
        "다음 문맥을 참조하여 질문에 답변하세요.\n\n[문맥]\n{}\n\n[질문]\n{}",
        if context.is_empty() { "관련 검색 데이터 없음" } else { &context },
        question
    );
 
    let llm_res = client
        .post(format!("{}/v1/chat/completions", llm_url))
        .json(&serde_json::json!({
            "messages": [
                {"role": "system", "content": "너는 제공된 문맥을 기반으로 정확하게 답변하는 도우미이다."},
                {"role": "user", "content": prompt}
            ],
            "temperature": 0.7
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("LLM server error: {}", e)))?;
 
    let llm_json: serde_json::Value = llm_res.json().await
        .map_err(|e| ServerFnError::new(format!("LLM response parsing error: {}", e)))?;
        
    let answer = llm_json["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("답변을 생성하지 못했습니다.")
        .to_string();
 
    Ok(answer)
}
 
#[component]
pub fn App() -> impl IntoView {
    let (question, set_question) = signal(String::new());
    let ask_action = ServerAction::<AskRag>::new();
 
    let pending = ask_action.pending();
    let value = ask_action.value();
 
    let on_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();
        if !question.get().trim().is_empty() {
            ask_action.dispatch(AskRag { question: question.get() });
        }
    };
 
    view! {
        <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif; padding: 20px;">
            <h2>"Leptos + PostgreSQL + pgvector RAG (Docker)"</h2>
            <form on:submit=on_submit style="display: flex; gap: 10px; margin-bottom: 20px;">
                <input
                    type="text"
                    style="flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"
                    placeholder="질문을 입력하세요..."
                    prop:value=question
                    on:input=move |e| set_question.set(event_target_value(&e))
                />
                <button type="submit" disabled=move || pending.get() style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    {move || if pending.get() { "검색 중..." } else { "질문하기" }}
                </button>
            </form>
 
            <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #e9ecef; min-height: 100px;">
            {move || match value.get() {
                    None => view! { <p style="color: #6c757d;">{"질문을 입력하세요.".to_string()}</p> }.into_view(),
                    Some(Ok(ans)) => view! { <p style="white-space: pre-wrap;">{ans}</p> }.into_view(),
                    Some(Err(err)) => view! { <p style="color: red;">{format!("오류: {}", err)}</p> }.into_view(),
                }}
            </div>
        </div>
    }
}

2. .env 파일 수정/생성

프로젝트 루트의 .env 파일에서 OPENAI_API_KEY 항목을 삭제하고, 로컬 폐쇄망 내부 컨테이너 주소를 정의합니다.

코드 스니펫

DATABASE_URL=postgres://postgres:postgres@localhost:5432/ragdb
EMBEDDING_URL=http://localhost:8081
LLM_URL=http://localhost:8080

(Docker Compose 네트워크 상에서 WASM/Backend 컨테이너가 직접 접근하는 환경이라면 http://embedding:80 http://llama:8080 형태로 변경할 있습니다.)

3. init.sql 수정 (임베딩 차원 변경)

OpenAI text-embedding-3-small 1536차원이지만, 로컬 all-MiniLM-L6-v2 모델은 384차원입니다. DB 스키마 차원도 맞춰주어야 pgvector 에러가 발생하지 않습니다.

SQL

-- pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector; 
 
-- 문서 저장 테이블 (차원을 1536 -> 384로 수정)
CREATE TABLE IF NOT EXISTS documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    embedding VECTOR(384) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
); 
 
-- Cosine Distance HNSW 인덱스 생성
CREATE INDEX IF NOT EXISTS docs_embedding_hnsw_idx
ON documents USING hnsw (embedding vector_cosine_ops);

4. docker-compose.yml 서비스 추가 ( 컨테이너 추가 )

만약 Leptos 서비스도 Docker Compose 같이 띄워 폐쇄망 내부 네트워크로 묶으려면 docker-compose.yml 끝에 서비스를 추가해 줍니다.

YAML

  web:
    build: .
    container_name: rag_app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
      - EMBEDDING_URL=http://embedding:80
      - LLM_URL=http://llama:8080
    depends_on:
      - db
      - embedding
      - llama

5. 재경량화 적용 방법

수정을 완료한 기존 PostgreSQL DB 볼륨의 임베딩 차원을 업데이트하고 서버를 재시작합니다.

Bash

# 1. 기존 DB 데이터 초기화 (차원이 1536에서 384로 변경되었기 때문)
docker-compose down -v
 
# 2. 로컬 서비스 및 컨테이너 재시작
docker-compose up -d
 
# 3. Leptos 앱 다시 빌드 및 실행
cargo leptos watch

"use leptos::prelude::*;

 

pub fn shell(options: LeptosOptions) -> impl IntoView {

    view! {

        <!DOCTYPE html>

        <html lang="ko">

            <head>

                <meta charset="utf-8"/>

                <meta name="viewport" content="width=device-width, initial-scale=1"/>

                <AutoReload options=options.clone() />

                <HydrationScripts options=options />

            </head>

            <body>

                <App/>

            </body>

        </html>

    }

}

 

// Leptos Server Function: 질문 백엔드에서 벡터 검색 로컬 LLM 답변 생성

#[server(AskRag, "/api")]

pub async fn ask_rag(question: String) -> Result<String, ServerFnError> {

    use pgvector::Vector;

    use sqlx::PgPool;

    use std::env;

 

    dotenvy::dotenv().ok();

    let db_url = env::var("DATABASE_URL").map_err(|_| ServerFnError::new("DATABASE_URL missing"))?;

    

    // 폐쇄망 서비스 엔드포인트 환경변수 (기본값 설정)

    let embed_url = env::var("EMBEDDING_URL").unwrap_or_else(|_| "http://localhost:8081".to_string());

    let llm_url = env::var("LLM_URL").unwrap_or_else(|_| "http://localhost:8080".to_string());

 

    let pool = PgPool::connect(&db_url).await.map_err(|e| ServerFnError::new(e.to_string()))?;

    let client = reqwest::Client::new();

 

    // 1. 질문을 로컬 TEI(Text Embeddings Inference) 서버를 통해 벡터로 변환 (all-MiniLM-L6-v2: 384차원)

    let embed_res = client

        .post(format!("{}/embed", embed_url))

        .json(&serde_json::json!({

            "inputs": question

        }))

        .send()

        .await

        .map_err(|e| ServerFnError::new(format!("Embedding server error: {}", e)))?;

 

    let vec_data: Vec<Vec<f32>> = embed_res.json().await

        .map_err(|e| ServerFnError::new(format!("Embedding parsing error: {}", e)))?;

    

    let query_vec_data = vec_data.first()

        .ok_or_else(|| ServerFnError::new("Empty embedding response"))?;

    let query_vector = Vector::from(query_vec_data.clone());

 

    // 2. pgvector Cosine similarity 검색

    let vec_str = format!(

        "[{}]",

        query_vector

             .to_vec()

             .iter()

             .map(|f| f.to_string())

             .collect::<Vec<String>>()

             .join(",")

    );

 

    let docs: Vec<String> = sqlx::query_scalar(

        r#"

        SELECT content

        FROM documents

        ORDER BY embedding <-> $1::vector

        LIMIT 3

        "#

    )

    .bind(vec_str)

    .fetch_all(&pool)

    .await

    .map_err(|e| ServerFnError::new(e.to_string()))?;

 

    let context = docs.join("\n---\n");

 

    // 3. Prompt 구성 로컬 llama.cpp 서버 (Chat Completions) 호출

    let prompt = format!(

        "다음 문맥을 참조하여 질문에 답변하세요.\n\n[문맥]\n{}\n\n[질문]\n{}",

        if context.is_empty() { "관련 검색 데이터 없음" } else { &context },

        question

    );

 

    let llm_res = client

        .post(format!("{}/v1/chat/completions", llm_url))

        .json(&serde_json::json!({

            "messages": [

                {"role": "system", "content": "너는 제공된 문맥을 기반으로 정확하게 답변하는 도우미이다."},

                {"role": "user", "content": prompt}

            ],

            "temperature": 0.7

        }))

        .send()

        .await

        .map_err(|e| ServerFnError::new(format!("LLM server error: {}", e)))?;

 

    let llm_json: serde_json::Value = llm_res.json().await

        .map_err(|e| ServerFnError::new(format!("LLM response parsing error: {}", e)))?;

        

    let answer = llm_json["choices"][0]["message"]["content"]

        .as_str()

        .unwrap_or("답변을 생성하지 못했습니다.")

        .to_string();

 

    Ok(answer)

}

 

#[component]

pub fn App() -> impl IntoView {

    let (question, set_question) = signal(String::new());

    let ask_action = ServerAction::<AskRag>::new();

 

    let pending = ask_action.pending();

    let value = ask_action.value();

 

    let on_submit = move |ev: leptos::ev::SubmitEvent| {

        ev.prevent_default();

        if !question.get().trim().is_empty() {

            ask_action.dispatch(AskRag { question: question.get() });

        }

    };

 

    view! {

        <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif; padding: 20px;">

            <h2>"Leptos + PostgreSQL + pgvector RAG (Docker)"</h2>

            <form on:submit=on_submit style="display: flex; gap: 10px; margin-bottom: 20px;">

                <input

                    type="text"

                    style="flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"

                    placeholder="질문을 입력하세요..."

                    prop:value=question

                    on:input=move |e| set_question.set(event_target_value(&e))

                />

                <button type="submit" disabled=move || pending.get() style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">

                    {move || if pending.get() { "검색 ..." } else { "질문하기" }}

                </button>

            </form>

 

            <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #e9ecef; min-height: 100px;">

            {move || match value.get() {

                    None => view! { <p style="color: #6c757d;">{"질문을 입력하세요.".to_string()}</p> }.into_view(),

                    Some(Ok(ans)) => view! { <p style="white-space: pre-wrap;">{ans}</p> }.into_view(),

                    Some(Err(err)) => view! { <p style="color: red;">{format!("오류: {}", err)}</p> }.into_view(),

                }}

            </div>

        </div>

    }

}"코드에서 사용자 ip dhcp라서 바뀔 있는데 이를 고려해 관련 코드를 전체 수정해줘

사용자 IP(DHCP) 변환에 영향을 받지 않고 폐쇄망 내에서 안정적으로 동작하도록 시스템을 구조화하려면, 코드 내부에서 특정 IP(172.24.27.59 ) 직접 참조하지 않고 Docker 네트워크 DNS 서비스명 또는 환경변수/동적 바인딩 구조로 정리해야 합니다.

1. 문제 분석 수정 방향

1.    서비스 통신 (Container-to-Container):

o   Rust 백엔드가 LLM, Embedding, PostgreSQL 통신할 고정 IP 대신 Docker Compose 서비스 이름(llama, embedding, db) 사용해야 IP 바뀌어도 통신에 지장이 없습니다.

2.    서버 바인딩 (Axum/Leptos):

o   서버 바인딩 IP 특정 IP 대신 0.0.0.0:3000으로 지정하여, 클라이언트의 IP(DHCP) 바뀌더라도 호스트 네트워크 인터페이스를 통해 정상 진입할 있도록 합니다.

3.    환경변수 유연성 확보:

o   .env 코드 기본값을 IP 기반이 아닌 DNS/hostname 기반으로 구성합니다.

2. 전체 수정 코드 (src/app.rs)

app.rs 내에서 로컬 서비스 접근 URL 디폴트값을 Docker 컨테이너 서비스 이름(http://embedding:80, http://llama:8080)으로 처리하고, 필요시 호스트 통신(localhost) 커버할 있게 환경변수 fallback 정리한 전체 코드입니다.

Rust

use leptos::prelude::*;
 
pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="ko">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options=options />
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}
 
// Leptos Server Function: 질문 시 백엔드에서 벡터 검색 및 로컬 LLM 답변 생성
#[server(AskRag, "/api")]
pub async fn ask_rag(question: String) -> Result<String, ServerFnError> {
    use pgvector::Vector;
    use sqlx::PgPool;
    use std::env;
 
    dotenvy::dotenv().ok();
    
    // IP 변동에 영향을 받지 않도록 Docker DNS 기반 기본값 선언
    // 호스트 직착 시 .env의 EMBEDDING_URL / LLM_URL / DATABASE_URL을 통해 덮어쓸 수 있습니다.
    let db_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://postgres:postgres@db:5432/ragdb".to_string());
    let embed_url = env::var("EMBEDDING_URL")
        .unwrap_or_else(|_| "http://embedding:80".to_string());
    let llm_url = env::var("LLM_URL")
        .unwrap_or_else(|_| "http://llama:8080".to_string());
 
    let pool = PgPool::connect(&db_url)
        .await
        .map_err(|e| ServerFnError::new(format!("DB connection failed: {}", e)))?;
    let client = reqwest::Client::new();
 
    // 1. 질문을 로컬 TEI(Text Embeddings Inference) 서버를 통해 벡터로 변환 (all-MiniLM-L6-v2: 384차원)
    let embed_res = client
        .post(format!("{}/embed", embed_url))
        .json(&serde_json::json!({
            "inputs": question
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("Embedding server error: {}", e)))?;
 
    let vec_data: Vec<Vec<f32>> = embed_res
        .json()
        .await
        .map_err(|e| ServerFnError::new(format!("Embedding parsing error: {}", e)))?;
    
    let query_vec_data = vec_data
        .first()
        .ok_or_else(|| ServerFnError::new("Empty embedding response"))?;
    let query_vector = Vector::from(query_vec_data.clone());
 
    // 2. pgvector Cosine similarity 검색
    let vec_str = format!(
        "[{}]",
        query_vector
             .to_vec()
             .iter()
             .map(|f| f.to_string())
             .collect::<Vec<String>>()
             .join(",")
    );
 
    let docs: Vec<String> = sqlx::query_scalar(
        r#"
        SELECT content
        FROM documents
        ORDER BY embedding <-> $1::vector
        LIMIT 3
        "#
    )
    .bind(vec_str)
    .fetch_all(&pool)
    .await
    .map_err(|e| ServerFnError::new(format!("Vector query error: {}", e)))?;
 
    let context = docs.join("\n---\n");
 
    // 3. Prompt 구성 및 로컬 llama.cpp 서버 (Chat Completions) 호출
    let prompt = format!(
        "다음 문맥을 참조하여 질문에 답변하세요.\n\n[문맥]\n{}\n\n[질문]\n{}",
        if context.is_empty() { "관련 검색 데이터 없음" } else { &context },
        question
    );
 
    let llm_res = client
        .post(format!("{}/v1/chat/completions", llm_url))
        .json(&serde_json::json!({
            "messages": [
                {"role": "system", "content": "너는 제공된 문맥을 기반으로 정확하게 답변하는 도우미이다."},
                {"role": "user", "content": prompt}
            ],
            "temperature": 0.7
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("LLM server error: {}", e)))?;
 
    let llm_json: serde_json::Value = llm_res
        .json()
        .await
        .map_err(|e| ServerFnError::new(format!("LLM response parsing error: {}", e)))?;
        
    let answer = llm_json["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("답변을 생성하지 못했습니다.")
        .to_string();
 
    Ok(answer)
}
 
#[component]
pub fn App() -> impl IntoView {
    let (question, set_question) = signal(String::new());
    let ask_action = ServerAction::<AskRag>::new();
 
    let pending = ask_action.pending();
    let value = ask_action.value();
 
    let on_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();
        if !question.get().trim().is_empty() {
            ask_action.dispatch(AskRag { question: question.get() });
        }
    };
 
    view! {
        <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif; padding: 20px;">
            <h2>"Leptos + PostgreSQL + pgvector RAG (Docker)"</h2>
            <form on:submit=on_submit style="display: flex; gap: 10px; margin-bottom: 20px;">
                <input
                    type="text"
                    style="flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"
                    placeholder="질문을 입력하세요..."
                    prop:value=question
                    on:input=move |e| set_question.set(event_target_value(&e))
                />
                <button type="submit" disabled=move || pending.get() style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    {move || if pending.get() { "검색 중..." } else { "질문하기" }}
                </button>
            </form>
 
            <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #e9ecef; min-height: 100px;">
            {move || match value.get() {
                    None => view! { <p style="color: #6c757d;">{"질문을 입력하세요.".to_string()}</p> }.into_view(),
                    Some(Ok(ans)) => view! { <p style="white-space: pre-wrap;">{ans}</p> }.into_view(),
                    Some(Err(err)) => view! { <p style="color: red;">{format!("오류: {}", err)}</p> }.into_view(),
                }}
            </div>
        </div>
    }
}

3. 관련 파일 설정 변경 (DHCP IP 독립화)

1) .env 설정

로컬 개발(바이너리 실행) 시와 Docker Compose 구동 시에 따라 도메인을 바꿀 있게 환경변수를 설정합니다.

·        Docker 컨테이너 내부 실행 (.env 기본 권장):

코드 스니펫

DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
EMBEDDING_URL=http://embedding:80
LLM_URL=http://llama:8080
LEPTOS_SITE_ADDR=0.0.0.0:3000

·        cargo leptos watch 호스트에서 직접 실행 :

코드 스니펫

DATABASE_URL=postgres://postgres:postgres@localhost:5432/ragdb
EMBEDDING_URL=http://localhost:8081
LLM_URL=http://localhost:8080
LEPTOS_SITE_ADDR=0.0.0.0:3000

2) src/main.rs 바인딩 설정 점검

main.rs에서 서버가 특정 IP 바인딩되지 않고 0.0.0.0으로 수신 대기하도록 되어 있는지 확인합니다.

Rust

let conf = get_configuration(None).unwrap();
let leptos_options = conf.leptos_options; 
// site_addr이 Cargo.toml이나 .env에서 0.0.0.0:3000으로 설정되어 있어야 
// 외부 DHCP IP 변경 시에도 접속을 허용합니다.
let addr = leptos_options.site_addr;

4. 접속 방법 (IP 변경 대응)

서버 바인딩을 0.0.0.0:3000으로 설정했으므로, PC DHCP IP 추후 변경되더라도 접속 변경된 IP 그대로 사용할 있습니다.

·        로컬 접속: http://localhost:3000 또는 [http://127.0.0.1:3000](http://127.0.0.1:3000)

·        폐쇄망 내부 PC 접근: `

"Docker 컨테이너 내부 실행 " "cargo leptos watch 호스트에서 직접 실행" 에서 windows11 powershell에서 windwos가상화 wsl 우분투로 접속할 경우, 그리고 향후 외부 환경에서 접속할 있도록 구성할 경우 어느 것을 선택히야 하나?

결론부터 말씀드리면, 환경을 상황과 목적에 맞춰 함께 활용하는 구조 가져가시는 것이 가장 좋습니다.

구체적으로는 개발할 때는 "호스트/WSL 직접 실행(cargo leptos watch)", 배포 외부 접속 운영 환경에서는 "Docker 컨테이너 실행" 선택해야 합니다.

1. 개발 테스트 단계: cargo leptos watch (WSL 직접 실행)

Windows 11 PowerShell에서 WSL(Ubuntu) 접속해 작업하는 개발 단계에서는 cargo leptos watch 방식이 훨씬 유리합니다.

·        코드 수정 즉시 반영 (Hot Reload): 소스 코드를 수정할 때마다 Docker 이미지 재빌드 과정 없이 바로 재컴파일 적용됩니다.

·        디버깅 용이성: WSL 내부 프로세스로 직접 실행되므로 터미널에서 컴파일 에러나 서버 로그를 빠르게 확인할 있습니다.

·        개발 접속 주소:

o   Windows 브라우저에서 http://localhost:3000으로 접속 가능합니다. (WSL2 Windows 호스트와의 포트 포워딩을 자동으로 처리해 줍니다.)

2. 향후 외부 환경 접속 검증 단계: Docker 컨테이너 실행

개발이 완료된 앱을 폐쇄망 PC 외부 네트워크에서 접근할 있도록 서빙하거나 검증할 때는 Docker 컨테이너 실행 방식 사용해야 합니다.

·        환경 격리 이식성: Rust 컴파일 환경, 라이브러리 버전, PostgreSQL/LLM/Embedding 컨테이너와의 네트워크 구성이 하나로 묶여 있어 다른 서버나 PC 옮겨도 동일하게 동작합니다.

·        DHCP IP 변경 대응: Docker 내부 네트워크(db, llama, embedding) 이용하므로, 외부에서 바라보는 서버 IP(DHCP) 바뀌더라도 컨테이너 상호 통신에는 영향을 받지 않습니다.

·        외부 접속 처리:

o   Docker 컨테이너에서 0.0.0.0:3000으로 포트를 바인딩해 두면, 외부 사용자(또는 다른 PC) http://<현재_서버_IP>:3000으로 접속할 있습니다.

3. 환경을 유연하게 오가는 구성 방법

.env 파일이나 환경변수를 활용해 개발 환경과 Docker 운영 환경을 손쉽게 스위칭할 있도록 구성하는 것을 추천합니다.

Docker 실행용 설정 (docker-compose.yml 사용 )

docker-compose.yml 내부에서 컨테이너 통신용 서비스 이름을 바라보도록 환경변수를 주입합니다.

YAML

  web:
    build: .
    container_name: rag_app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
      - EMBEDDING_URL=http://embedding:80
      - LLM_URL=http://llama:8080
      - LEPTOS_SITE_ADDR=0.0.0.0:3000
    depends_on:
      - db
      - embedding
      - llama

WSL 터미널에서 개발용 실행 (cargo leptos watch)

WSL 로컬 환경(.env)에서는 PostgreSQL, TEI, Llama.cpp 포트가 호스트로 노출(5432, 8081, 8080)되어 있으므로 localhost 바라보도록 설정합니다.

.env

코드 스니펫

DATABASE_URL=postgres://postgres:postgres@localhost:5432/ragdb
EMBEDDING_URL=http://localhost:8081
LLM_URL=http://localhost:8080
LEPTOS_SITE_ADDR=0.0.0.0:3000

4. 외부/폐쇄망 접속 주의사항 (Windows 11 / WSL2 환경)

Windows 11 WSL2 내부에서 실행되는 (0.0.0.0:3000) 외부 PC 접속하도록 하려면 아래 가지 작업이 필요합니다.

1.    Windows 방화벽 인바운드 규칙 추가:

o   Windows PowerShell(관리자 권한)에서 3000 포트를 허용해 줍니다.

PowerShell

New-NetFirewallRule -DisplayName "Leptos RAG App" -Direction Inbound -LocalPort 3000 -Protocol TCP -Action Allow

2.    WSL2 포트 포워딩 (필요 ):

o   Windows 11 최신 버전에서는 WSL2 mirrored 워크네트워킹 기능을 사용하거나, Windows IP 들어오는 3000 포트 요청을 WSL2 내부 IP 전달하도록 설정할 있습니다.

향후 전사 제품 판매를 목적으로 하고 싶다. Docker 실행용 설정을 사용하는게 맞나? 맞다면 이에 맞게 소스 환경을 전제 수정해줘

, 향후 전사 배포 제품 판매(상용화/SaaS/On-Premise 제품화) 목적이라면 Docker 실행용 설정을 표준으로 가져가는 것이 무조건 올바른 선택입니다.

제품 판매 환경에서는 다음과 같은 이유로 Docker 기반 상용화 구조가 필수적입니다:

1.    고객사 환경 독립성: 고객사의 OS 패키지 버전(Rust, C++ 라이브러리 ) 영향받지 않고 완벽히 격리되어 실행됩니다.

2.    폐쇄망 이식성: 인터넷이 차단된 폐쇄망 환경에서도 docker save / load 통해 컨테이너 이미지 형태로 제품을 번에 배포할 있습니다.

3.    내부 통신 보안 안정성: 외부 네트워크 IP(DHCP ) 변경되어도 DB, LLM, Embedding, Web 백엔드 통신은 Docker internal DNS(db, llama, embedding) 통해 항시 고정되어 안전하게 동작합니다.

전사 배포 상용 제품화를 고려하여 모든 소스 코드, 환경변수, Docker Compose, 셋업 스크립트 완전한 형태로 업데이트했습니다.

1. src/app.rs (컨테이너/제품 환경 최적화 소스)

컨테이너 환경변수를 우선적으로 참조하도록 기본값을 Docker 서비스명(db, embedding, llama)으로 상향 통일하고, 에러 메시지 처리를 더욱 명확하게 강화했습니다.

Rust

use leptos::prelude::*;
 
pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="ko">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options=options />
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}
 
// Leptos Server Function: 폐쇄망 내부 컨테이너 간 통신 기반 RAG 파이프라인
#[server(AskRag, "/api")]
pub async fn ask_rag(question: String) -> Result<String, ServerFnError> {
    use pgvector::Vector;
    use sqlx::PgPool;
    use std::env;
 
    dotenvy::dotenv().ok();
    
    // 제품/컨테이너 표준 내부 도메인 설정 (환경변수가 없을 경우 Docker 서비스 이름 사용)
    let db_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://postgres:postgres@db:5432/ragdb".to_string());
    let embed_url = env::var("EMBEDDING_URL")
        .unwrap_or_else(|_| "http://embedding:80".to_string());
    let llm_url = env::var("LLM_URL")
        .unwrap_or_else(|_| "http://llama:8080".to_string());
 
    let pool = PgPool::connect(&db_url)
        .await
        .map_err(|e| ServerFnError::new(format!("DB 접속 실패: {}", e)))?;
    let client = reqwest::Client::new();
 
    // 1. 질문을 로컬 TEI(Text Embeddings Inference) 컨테이너로 벡터화 (all-MiniLM-L6-v2: 384차원)
    let embed_res = client
        .post(format!("{}/embed", embed_url))
        .json(&serde_json::json!({
            "inputs": question
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("임베딩 서버 통신 에러: {}", e)))?;
 
    let vec_data: Vec<Vec<f32>> = embed_res
        .json()
        .await
        .map_err(|e| ServerFnError::new(format!("임베딩 응답 파싱 에러: {}", e)))?;
    
    let query_vec_data = vec_data
        .first()
        .ok_or_else(|| ServerFnError::new("임베딩 결과를 수신하지 못했습니다."))?;
    let query_vector = Vector::from(query_vec_data.clone());
 
    // 2. pgvector Cosine similarity 유사도 검색
    let vec_str = format!(
        "[{}]",
        query_vector
             .to_vec()
             .iter()
             .map(|f| f.to_string())
             .collect::<Vec<String>>()
             .join(",")
    );
 
    let docs: Vec<String> = sqlx::query_scalar(
        r#"
        SELECT content
        FROM documents
        ORDER BY embedding <-> $1::vector
        LIMIT 3
        "#
    )
    .bind(vec_str)
    .fetch_all(&pool)
    .await
    .map_err(|e| ServerFnError::new(format!("벡터 검색 에러: {}", e)))?;
 
    let context = docs.join("\n---\n");
 
    // 3. Prompt 구성 및 로컬 llama.cpp 컨테이너 호출
    let prompt = format!(
        "다음 문맥을 참조하여 질문에 답변하세요.\n\n[문맥]\n{}\n\n[질문]\n{}",
        if context.is_empty() { "관련 검색 데이터 없음" } else { &context },
        question
    );
 
    let llm_res = client
        .post(format!("{}/v1/chat/completions", llm_url))
        .json(&serde_json::json!({
            "messages": [
                {"role": "system", "content": "너는 제공된 문맥을 기반으로 정확하게 답변하는 도우미이다."},
                {"role": "user", "content": prompt}
            ],
            "temperature": 0.7
        }))
        .send()
        .await
        .map_err(|e| ServerFnError::new(format!("LLM 서버 통신 에러: {}", e)))?;
 
    let llm_json: serde_json::Value = llm_res
        .json()
        .await
        .map_err(|e| ServerFnError::new(format!("LLM 응답 파싱 에러: {}", e)))?;
        
    let answer = llm_json["choices"][0]["message"]["content"]
        .as_str()
        .unwrap_or("답변을 생성하지 못했습니다.")
        .to_string();
 
    Ok(answer)
}
 
#[component]
pub fn App() -> impl IntoView {
    let (question, set_question) = signal(String::new());
    let ask_action = ServerAction::<AskRag>::new();
 
    let pending = ask_action.pending();
    let value = ask_action.value();
 
    let on_submit = move |ev: leptos::ev::SubmitEvent| {
        ev.prevent_default();
        if !question.get().trim().is_empty() {
            ask_action.dispatch(AskRag { question: question.get() });
        }
    };
 
    view! {
        <div style="max-width: 600px; margin: 40px auto; font-family: sans-serif; padding: 20px;">
            <h2>"Leptos + PostgreSQL + pgvector RAG (Enterprise)"</h2>
            <form on:submit=on_submit style="display: flex; gap: 10px; margin-bottom: 20px;">
                <input
                    type="text"
                    style="flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px;"
                    placeholder="질문을 입력하세요..."
                    prop:value=question
                    on:input=move |e| set_question.set(event_target_value(&e))
                />
                <button type="submit" disabled=move || pending.get() style="padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">
                    {move || if pending.get() { "검색 중..." } else { "질문하기" }}
                </button>
            </form>
 
            <div style="background: #f8f9fa; padding: 15px; border-radius: 4px; border: 1px solid #e9ecef; min-height: 100px;">
            {move || match value.get() {
                    None => view! { <p style="color: #6c757d;">{"질문을 입력하세요.".to_string()}</p> }.into_view(),
                    Some(Ok(ans)) => view! { <p style="white-space: pre-wrap;">{ans}</p> }.into_view(),
                    Some(Err(err)) => view! { <p style="color: red;">{format!("오류: {}", err)}</p> }.into_view(),
                }}
            </div>
        </div>
    }
}

2. docker-compose.yml (전사 제품 배포 표준 구성)

RAG 서비스(web), DB(db), 임베딩(embedding), LLM(llama) 단일 격리 네트워크(rag-network) 묶어 제품화 가능한 스택으로 구성합니다.

YAML

services:
  # 1. RAG Web App
  web:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: rag_app
    restart: always
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
      - EMBEDDING_URL=http://embedding:80
      - LLM_URL=http://llama:8080
      - LEPTOS_SITE_ADDR=0.0.0.0:3000
    depends_on:
      - db
      - embedding
      - llama
    networks:
      - rag-network
 
  # 2. Vector DB (PostgreSQL + pgvector)
  db:
    image: pgvector/pgvector:pg16
    container_name: rag_postgres
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: ragdb
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - rag-network
 
  # 3. Embedding Server
  embedding:
    image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.2
    container_name: embedding_server
    restart: always
    ports:
      - "8081:80"
    environment:
      - MODEL_ID=sentence-transformers/all-MiniLM-L6-v2
    volumes:
      - ./embeddings_cache:/data
    networks:
      - rag-network
 
  # 4. Local LLM Server (llama.cpp)
  llama:
    image: ghcr.io/ggml-org/llama.cpp:server
    container_name: llama_server
    restart: always
    ports:
      - "8080:8080"
    volumes:
      - /home/linux/models:/models
    command:
      - -m
      - /models/qwen3-8b/Qwen_Qwen3.5-2B-Q8_0.gguf
      - -c
      - "4096"
      - --host
      - 0.0.0.0
      - --port
      - "8080"
    deploy:
      resources:
        limits:
          memory: 8G
    networks:
      - rag-network
 
networks:
  rag-network:
    driver: bridge
 
volumes:
  pgdata:

3. init.sql (384차원 모델 적용)

all-MiniLM-L6-v2 모델 규격에 맞춰 VECTOR(384) 초기화 구성을 설정합니다.

SQL

-- pgvector 확장 활성화
CREATE EXTENSION IF NOT EXISTS vector; 
 
-- 문서 저장 테이블 (all-MiniLM-L6-v2 384차원)
CREATE TABLE IF NOT EXISTS documents (
    id BIGSERIAL PRIMARY KEY,
    content TEXT NOT NULL,
    embedding VECTOR(384) NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
); 
 
-- Cosine Distance HNSW 인덱스 생성
CREATE INDEX IF NOT EXISTS docs_embedding_hnsw_idx
ON documents USING hnsw (embedding vector_cosine_ops);

4. .env (제품 운영 표준 설정)

코드 스니펫

DATABASE_URL=postgres://postgres:postgres@db:5432/ragdb
EMBEDDING_URL=http://embedding:80
LLM_URL=http://llama:8080
LEPTOS_SITE_ADDR=0.0.0.0:3000

5. Windows 11 방화벽 설정 (외부 PC 접근용)

고객사 내부망이나 전사 사용자 PC에서 해당 개발/운영 PC 접속할 있도록 Windows 11 PowerShell(관리자 권한)에서 인바운드 방화벽 포트를 개방해 줍니다.

PowerShell

New-NetFirewallRule -DisplayName "My RAG App Product Server" -Direction Inbound -LocalPort 3000 -Protocol TCP -Action Allow

6. 서버 전체 재구동 절차

아래 명령어로 기존 컨테이너 DB 스키마 차원 구성을 깨끗이 재정의하고 전체 제품 스택을 번에 구동시킵니다.

Bash

cd /home/linux/projects/my-rag-app
 
# 1. 기존 컨테이너 및 DB 볼륨 초기화
docker-compose down -v
 
# 2. 전체 스택 빌드 및 실행 (웹 앱 + DB + 임베딩 + LLM)
docker compose up --build -d
 
# 3. 실행 상태 확인
docker-compose ps

구성이 완비되면 사용자 PC DHCP IP 바뀌거나, 시스템이 다른 폐쇄망 서버로 이관되더라도 docker-compose up -d 줄만으로 전사 배포 외부 서빙이 가능한 상태가 됩니다.

"root@DESKTOP-GK0BRBG:/home/linux/projects/my-rag-app# docker-compose up --build -d

Building web

[+] Building 384.3s (20/20) FINISHED                                                                                                                                                                                          docker:default

 => [internal] load build definition from Dockerfile                                                                                                                                                                                    0.0s

 => => transferring dockerfile: 964B                                                                                                                                                                                                    0.0s

 => WARN: FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)                                                                                                                                                          0.0s

 => [internal] load metadata for docker.io/library/debian:bookworm-slim                                                                                                                                                                 0.8s

 => [internal] load metadata for docker.io/library/rust:latest                                                                                                                                                                          0.8s

 => [internal] load .dockerignore                                                                                                                                                                                                       0.0s

 => => transferring context: 2B                                                                                                                                                                                                         0.0s

 => [builder 1/8] FROM docker.io/library/rust:latest@sha256:3382bd20aa942806c533e9a73cd000474fb3ef173f71e684cc9b942675781769                                                                                                            0.0s

 => => resolve docker.io/library/rust:latest@sha256:3382bd20aa942806c533e9a73cd000474fb3ef173f71e684cc9b942675781769                                                                                                                    0.0s

 => [internal] load build context                                                                                                                                                                                                      74.2s

 => => transferring context: 8.25GB                                                                                                                                                                                                    74.0s

 => [stage-1 1/6] FROM docker.io/library/debian:bookworm-slim@sha256:abd67ffcfa541b485a3dff59865ab629aa048a6c613e639d36e7456b0b229241                                                                                                   0.0s

 => => resolve docker.io/library/debian:bookworm-slim@sha256:abd67ffcfa541b485a3dff59865ab629aa048a6c613e639d36e7456b0b229241                                                                                                           0.0s

 => CACHED [builder 2/8] RUN apt-get update && apt-get install -y     pkg-config     libssl-dev     curl     git     && rm -rf /var/lib/apt/lists/*                                                                                     0.0s

 => CACHED [builder 3/8] RUN rustup target add wasm32-unknown-unknown                                                                                                                                                                   0.0s

 => CACHED [builder 4/8] RUN curl -sSfL https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xzv -C /usr/local/cargo/bin/ &&     cargo binstall -y cargo-leptos -  0.0s

 => CACHED [builder 5/8] WORKDIR /app                                                                                                                                                                                                   0.0s

 => [builder 6/8] COPY . .                                                                                                                                                                                                             47.7s

 => [builder 7/8] RUN cargo update -p wasm-bindgen --precise 0.2.99                                                                                                                                                                     5.9s

 => [builder 8/8] RUN cargo leptos build --release                                                                                                                                                                                    253.6s

 => CACHED [stage-1 2/6] RUN apt-get update && apt-get install -y ca-certificates libssl-dev && rm -rf /var/lib/apt/lists/*                                                                                                             0.0s

 => CACHED [stage-1 3/6] WORKDIR /app                                                                                                                                                                                                   0.0s

 => [stage-1 4/6] COPY --from=builder /app/target/release/my-rag-app /app/                                                                                                                                                              0.1s

 => [stage-1 5/6] COPY --from=builder /app/target/site /app/site                                                                                                                                                                        0.1s

 => [stage-1 6/6] COPY --from=builder /app/.env /app/.env                                                                                                                                                                               0.1s

 => exporting to image                                                                                                                                                                                                                  0.9s

 => => exporting layers                                                                                                                                                                                                                 0.5s

 => => exporting manifest sha256:ccb39887b05563522ab65b477bbe0736022008e2a3588b925adae89a13bc5dd4                                                                                                                                       0.0s

 => => exporting config sha256:e6dbb3188fc15f570d5adc2cea66e6ce40194286ec07d2cfa781842b7e6090e2                                                                                                                                         0.0s

 => => exporting attestation manifest sha256:f982fefe1f3b785c9a303c852bed413f071e4d3864f4a8e1212a57f488aaf926                                                                                                                           0.0s

 => => exporting manifest list sha256:b5fae74240a8a594bd08b9428518c4c4edd2a6a16fff941f5206a2a5bc7f9d23                                                                                                                                  0.0s

 => => naming to docker.io/library/my-rag-app_web:latest                                                                                                                                                                                0.0s

 => => unpacking to docker.io/library/my-rag-app_web:latest                                                                                                                                                                             0.2s

 

 1 warning found (use docker --debug to expand):

 - FromAsCasing: 'as' and 'FROM' keywords' casing do not match (line 1)

Creating llama_server     ... done

Creating rag_postgres     ... done

Creating embedding_server ... done

Creating rag_app          ...

Creating rag_app          ... error

 

ERROR: for rag_app  Cannot start service web: failed to set up container networking: driver failed programming external connectivity on endpoint rag_app (6eb40b7e75f510d4b2e4d450692fb5116eec3dedd69ff0dd1d642a94b084a40a): failed to bind host port 0.0.0.0:3000/tcp: address already in use

 

ERROR: for web  Cannot start service web: failed to set up container networking: driver failed programming external connectivity on endpoint rag_app (6eb40b7e75f510d4b2e4d450692fb5116eec3dedd69ff0dd1d642a94b084a40a): failed to bind host port 0.0.0.0:3000/tcp: address already in use

ERROR: Encountered errors while bringing up the project.

root@DESKTOP-GK0BRBG:/home/linux/projects/my-rag-app#

" 해결 방법?

로그의 핵심 원인은 3000 포트가 이미 사용 (address already in use)이라 Docker 컨테이너(rag_app) 시작되지 못한 것입니다.

WSL 백그라운드나 호스트 시스템에서 이전에 실행한 Leptos 프로세스(cargo leptos watch ) 또는 기존에 있는 다른 서버 3000 포트를 점유하고 있기 때문에 발생합니다.

해결 절차

1단계: 3000 포트를 점유 중인 프로세스 종료

WSL 터미널에서 아래 명령어를 실행하여 3000 포트를 사용하는 프로세스를 찾아 종료합니다.

Bash

# 1. 3000번 포트를 사용 중인 PID(프로세스 ID) 확인
sudo lsof -i :3000
 
# 또는 fuser로 3000번 포트를 점유 중인 프로세스 즉시 강제 종료 (가장 간단)
sudo fuser -k 3000/tcp

참고: fuser 명령어가 없다면 sudo apt install psmisc 설치하거나, lsof에서 확인된 PID sudo kill -9 <PID> 직접 종료해 주세요.

2단계: 기존 Docker 컨테이너 정리

남아있는 Docker 컨테이너 상태를 정리합니다.

Bash

docker-compose down

3단계: Docker Compose 재구동

포트 점유가 해제된 다시 컨테이너를 올립니다. 이미지 빌드는 이미 성공했으므로 --build 옵션 없이 빠르게 구동할 있습니다.

Bash

docker-compose up -d

정상 구동 확인

컨테이너들이 오류 없이 정상 실행 중인지 확인합니다.

Bash

docker-compose ps

모든 컨테이너(rag_app, rag_postgres, embedding_server, llama_server) STATUS Up으로 표시되면 완료입니다.

💡 원인 예방

이전 단계에서 안내해 드린 cargo leptos watch 터미널에서 따로 실행 중이셨다면, Docker 컨테이너로 앱을 띄울 때는 로컬 cargo leptos watch Ctrl + C 먼저 종료해야 3000 포트 충돌이 일어나지 않습니다.

lsof , fuser 의마 설명해줘. 단어 전체 이름도 설명해줘

Linux Unix 계열 시스템에서 네트워크 포트나 파일 점유 문제를 해결할 핵심적으로 사용하는 명령어의 풀네임과 핵심 의미는 다음과 같습니다.

1. lsof (List Open Files)

📌 단어 전체 이름

List System Open Files (시스템의 열린 파일 목록 출력)

💡 의미와 핵심 용도

Unix/Linux 시스템에서는 "모든 것은 파일이다(Everything is a file)"라는 철학을 가지고 있습니다. 일반 문서 파일뿐만 아니라 네트워크 소켓(TCP/UDP 포트), 디바이스, 프로세스 통신(Pipe) 등도 시스템 내부적으로는 모두 파일 형태로 다루어집니다.

·        lsof 역할: 현재 시스템에서 프로세스들이 열어두고(Open) 있는 모든 파일의 목록 조회합니다.

·        주요 특징:

o   "누가 3000 포트(소켓 파일) 열어서 쓰고 있는가?"

o   "어떤 프로세스가 디렉터리/파일을 참조 중인가?"

o   특정 포트나 파일의 소유 프로세스, PID(프로세스 ID), 사용자 계정 등의 세부 정보를 상세하게 찾아낼 사용합니다.

Bash

# 예시: 3000번 포트를 사용 중인 프로세스 상세 확인
sudo lsof -i :3000

2. fuser (File User)

📌 단어 전체 이름

File User (파일 또는 포트 사용자)

💡 의미와 핵심 용도

fuser 이름 그대로 특정 파일, 디렉터리, 또는 네트워크 포트를 사용하고 있는 '사용자(프로세스)' 식별하는 도구입니다.

·        fuser 역할: 대상(파일/포트) 지정하면 이를 점유하고 있는 프로세스의 PID 목록을 빠르게 반환하며, 곧바로 해당 프로세스를 강제 종료(Kill)하는 특화되어 있습니다.

·        주요 특징:

o   정보 조회보다 "점유 중인 프로세스 타겟팅 즉시 종료" 최적화되어 있습니다.

o   -k (Kill) 옵션을 붙여 특정 포트를 점유하여 방해하는 프로세스를 번에 정리할 매우 유용합니다.

Bash

# 예시: 3000번 포트(TCP)를 점유한 프로세스를 즉시 강제 종료(Kill)
sudo fuser -k 3000/tcp

📊 요약 비교

구분 lsof (List Open Files) fuser (File User)
풀네임 List System Open Files File User
목적 상세한 정보 조회 (어떤 앱이, 어떤 계정으로, 무엇을 열었는지) 빠른 식별 종료 (누가 쓰는지 PID 확인 즉시 kill)
비유 파일/포트를 점유한 대상의 "상세 신원 진술서" 파일/포트를 점유한 대상을 "조준하여 제압하는 도구"

 

728x90