Open Source & Self-hosted RAG LLM Server with…

Open Source & Self-hosted RAG LLM Server with...

This article contains affiliate links. We may earn a commission at no extra cost to you. Full disclosure.




⚠ Duplicate check: This draft looks similar to an existing post (semantic match, 82% similarity) — Building a Local RAG System With Ollama and Qdrant: Complete Tutorial. Decide to merge, rewrite angle, or publish as follow-up before going live.

Forget the cloud API fees and vendor lock-in for your RAG (Retrieval Augmented Generation) pipelines. We're building a fully self-hosted, open-source RAG LLM server that puts you in complete control, from data ingestion to model inference. This isn't about theoretical possibilities; it's about practical implementation for builders who need to deploy AI-powered search and Q&A systems without breaking the bank or sacrificing data privacy. Imagine a scenario where you can ingest terabytes of internal documentation, customer support tickets, or proprietary research, and query it with a cutting-edge LLM, all running on your own hardware. We'll leverage Ollama for seamless model management, ChromaDB for efficient vector storage, and LangChain for orchestrating the RAG workflow. This setup allows for rapid iteration and deployment, crucial for staying ahead in the fast-paced AI development cycle. By the end of this guide, you'll have a robust, scalable RAG system ready to integrate into your applications, with a clear understanding of the performance characteristics and a roadmap for further optimization. This approach is particularly compelling when considering the projected growth of LLM inference costs, which are estimated to reach $100 billion annually by 2028, according to some industry analysts.

Dockerizing Your RAG Stack: The Foundation

To ensure reproducibility and ease of deployment, we'll containerize our entire RAG stack using Docker. This approach isolates dependencies and simplifies management across different environments. We'll need three core services: Ollama for serving LLMs, ChromaDB for vector storage, and a custom application container that orchestrates the RAG process. For Ollama, we'll use the official `ollama/ollama` image, which simplifies downloading and running various open-source LLMs. ChromaDB will be served by its own container using the `chromadb/chromadb` image. The third container will house our Python application, which will interact with both Ollama and ChromaDB. This layered approach ensures that each component can be managed and scaled independently. For example, if your vector database becomes a bottleneck, you can scale ChromaDB without affecting your LLM inference server. This modularity is key to building resilient AI systems.

Our `docker-compose.yml` file will define these services and their interdependencies. We'll map ports, define volumes for persistent data storage (crucial for ChromaDB and Ollama model files), and set up a shared network for seamless communication. For instance, the Ollama service will expose port 11434, and ChromaDB will expose port 8000. The application container will then connect to these services using their container names as hostnames within the Docker network. This eliminates the need for complex network configurations and allows us to refer to services like http://chromadb:8000 or http://ollama:11434 directly from our application code. This setup is a significant improvement over managing individual installations on a host machine, which often leads to dependency conflicts and versioning nightmares.

⭐ monitor

Check monitor →

Affiliate link

⭐ NordVPN

Top-rated VPN for online privacy and security. Lightning-fast servers.


Check NordVPN →

Affiliate link

⭐ Hostinger

Premium web hosting with 60% off. Trusted by millions worldwide.


Check Hostinger →

Affiliate link

Serving LLMs with Ollama: Model Flexibility

Ollama acts as our local LLM inference engine, providing a unified API for various open-source models. This is where we gain flexibility, allowing us to swap models like Llama 3.1 70B, Mistral Large, or even smaller, fine-tuned models based on our specific needs and hardware capabilities. The `ollama/ollama` Docker image makes this incredibly simple. Once Ollama is running, you can pull models directly using the `ollama pull` command. For example, to get Llama 3.1 70B, you'd run:


docker exec -it ollama ollama pull llama3.1

This command downloads the model weights and configuration into Ollama's data directory, which we'll have mounted as a volume in our `docker-compose.yml`. The Ollama API then exposes a RESTful interface, typically on port 11434, allowing our application to send prompts and receive completions. We can query the available models using curl http://localhost:11434/api/tags, which will list all the models currently available on our Ollama server. The latency for models like Llama 3.1 70B, when run on a capable GPU (e.g., an NVIDIA A100 with 80GB VRAM), can be as low as 500ms for a 500-token response, a stark contrast to cloud APIs that might introduce network latency on top of model processing time.

The choice of model significantly impacts performance and cost. While proprietary models like GPT-4o or Claude Sonnet offer state-of-the-art capabilities, their API calls accrue costs that can quickly become substantial for high-volume applications. For instance, a single query to GPT-4o can cost upwards of $0.01, and at millions of queries per month, this adds up. Open-source alternatives like Llama 3.1 70B, when self-hosted, have an upfront hardware cost but zero per-query inference cost. The trade-off is often in raw performance and the ease of use of managed APIs. However, for tasks that don't require the absolute bleeding edge of LLM capabilities, or where data privacy is paramount, self-hosting with Ollama provides a compelling economic and control advantage. We can also experiment with quantization techniques (e.g., Q4_K_M, Q5_K_M) to reduce the VRAM footprint and potentially increase inference speed on less powerful hardware, though this can sometimes lead to a slight degradation in output quality.

Vector Storage with ChromaDB: Efficient Retrieval

ChromaDB serves as our vector database, storing the embeddings of our documents and enabling efficient similarity search. This is the backbone of the retrieval part of RAG. We'll use the official `chromadb/chromadb` Docker image, ensuring a straightforward setup. Our `docker-compose.yml` will expose ChromaDB's API on port 8000. Crucially, we'll configure a persistent volume for ChromaDB to ensure that our indexed data is not lost when the container restarts. The default ChromaDB configuration stores data in /chroma within the container, so we'll map this to a host directory like ./chroma_data.

Connecting to ChromaDB from our application container is as simple as using its service name and port. In Python, using the `chromadb` client library, this looks like:


import chromadb

# Connect to the ChromaDB server running in a separate container
client = chromadb.HttpClient(host='chromadb', port=8000)

# Verify connection by listing collections
print(client.list_collections())

When you run this code within your application container, it will attempt to connect to the `chromadb` service on port 8000, which is exposed by the ChromaDB container. The output of `client.list_collections()` should show an empty list if this is your first time running, or any existing collections you've previously created. The performance of ChromaDB is highly dependent on the number of embeddings and the dimensionality of those embeddings. For datasets exceeding millions of vectors, consider optimizing ChromaDB's configuration or exploring distributed vector databases. However, for many internal RAG applications, a single instance of ChromaDB can handle billions of dimensions effectively, especially when paired with efficient embedding models.

Building the RAG Orchestration Layer

This is where LangChain shines. We'll build a Python application that orchestrates the entire RAG process: taking a user query, embedding it, querying ChromaDB for relevant documents, and then feeding both the query and the retrieved context to Ollama for a final answer. We'll use the `langchain-community` and `langchain-chroma` packages for integration. First, ensure you have the necessary libraries installed in your application container:


pip install langchain-community langchain-chroma langchain-ollama sentence-transformers

The core logic involves setting up the `Chroma` vector store client pointing to our running ChromaDB instance, and the `Ollama` LLM wrapper pointing to our Ollama server. We'll also need an embedding model. For a self-hosted setup, `sentence-transformers` offers excellent open-source options like `all-MiniLM-L6-v2` or `all-mpnet-base-v2`, which can be loaded locally.


from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import SentenceTransformerEmbeddings
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# 1. Initialize ChromaDB client and embeddings
vectorstore_client = chromadb.HttpClient(host='chromadb', port=8000)
embedding_function = SentenceTransformerEmbeddings(model_name="all-MiniLM-L6-v2")

# Ensure the collection exists or create it
collection_name = "my_rag_docs"
try:
    vectorstore = Chroma(
        client=vectorstore_client,
        collection_name=collection_name,
        embedding_function=embedding_function
    )
    print(f"Connected to existing collection: {collection_name}")
except Exception as e:
    print(f"Collection not found, creating: {collection_name}. Error: {e}")
    # If collection does not exist, create it
    vectorstore = Chroma.create_collection(
        name=collection_name,
        client=vectorstore_client,
        embedding_function=embedding_function
    )
    # In a real scenario, you would add documents here.
    # For this example, we assume it's pre-populated or will be populated elsewhere.

# 2. Initialize Ollama LLM
llm = Ollama(model="llama3.1", base_url="http://ollama:11434")

# 3. Define a prompt template
template = """Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say that you don't know, don't try to guess the answer.
Keep the answer as short as possible.

Context: {context}
Question: {question}

Helpful Answer:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)

# 4. Create the RAG chain
qa_chain = RetrievalQA.from_chain_type(
    llm,
    retriever=vectorstore.as_retriever(search_kwargs={"k": 3}), # Retrieve top 3 documents
    chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
)

# 5. Query the RAG system
user_query = "What are the benefits of self-hosted RAG?"
result = qa_chain({"query": user_query})
print(f"Query: {user_query}")
print(f"Answer: {result['result']}")

The `SentenceTransformerEmbeddings` will download the model weights locally the first time it's used, similar to Ollama. The `Chroma` client connects to our `chromadb` service. The `Ollama` wrapper connects to our `ollama` service. The `RetrievalQA` chain orchestrates the process: it takes the `user_query`, passes it to the `vectorstore` retriever to fetch relevant documents, and then constructs a prompt using `QA_CHAIN_PROMPT` with the retrieved `context` and the original `question`. This combined prompt is then sent to the `llm` (Ollama) for a final answer. The performance of this step depends heavily on the LLM's speed and the number of documents retrieved. For Llama 3.1 70B on a good GPU, generating an answer based on 3 retrieved chunks might take 2-5 seconds.

Data Ingestion and Embedding Strategies

The effectiveness of any RAG system hinges on the quality and relevance of the data it retrieves. For data ingestion, we need a robust process to load documents, split them into manageable chunks, and generate embeddings. LangChain provides document loaders for various formats (PDF, TXT, HTML, etc.) and text splitters (e.g., `RecursiveCharacterTextSplitter`). The choice of embedding model is critical. `all-MiniLM-L6-v2` is a good starting point, offering a balance of speed and performance, with embeddings of 384 dimensions. For higher accuracy, consider `all-mpnet-base-v2` (768 dimensions) or models from the BGE family. The dimensionality impacts storage size and search performance.

When ingesting data, consider chunking strategies. Overlapping chunks (e.g., 200-500 characters with 50-100 characters overlap) help preserve context across chunk boundaries. The size of the chunks also influences retrieval accuracy. Too small, and you lose context; too large, and you might retrieve irrelevant information within a large chunk. A typical chunk size for text might be between 500 and 1000 tokens. For example, to add documents to our ChromaDB collection:


from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter

# Load documents (e.g., from a PDF file)
loader = PyPDFLoader("path/to/your/document.pdf")
documents = loader.load()

# Split documents into chunks
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
chunks = text_splitter.split_documents(documents)

# Add chunks to ChromaDB
# Ensure vectorstore is initialized as shown in the previous section
collection_name = "my_rag_docs"
try:
    vectorstore = Chroma(
        client=vectorstore_client,
        collection_name=collection_name,
        embedding_function=embedding_function
    )
except Exception as e:
    print(f"Collection not found. Error: {e}")
    # Handle creation if necessary, or assume it's done.

# Add the documents to the collection
if chunks:
    vectorstore.add_documents(chunks)
    print(f"Added {len(chunks)} chunks to collection '{collection_name}'.")
else:
    print("No chunks to add.")

# Verify by counting documents
count_result = vectorstore_client.get_collection(collection_name).count()
print(f"Total documents in collection: {count_result}")

The `add_documents` operation will generate embeddings for each chunk using our `embedding_function` and store them along with the text content in ChromaDB. The time taken for this depends on the number of chunks and the speed of the embedding model. For 1000 chunks with `all-MiniLM-L6-v2`, this might take 30-60 seconds. The `count()` method provides a quick verification of the data ingestion.

Performance Benchmarking and Optimization

To truly understand the value of this self-hosted RAG setup, we need to benchmark its performance. Key metrics include:

  • LLM Inference Latency: Time taken for Ollama to generate a response given a prompt and context.
  • Retrieval Latency: Time taken for ChromaDB to return relevant document chunks.
  • Embedding Generation Time: Time taken to embed new documents.
  • End-to-End Query Time: Total time from user query to final answer.

We can instrument our Python application to measure these times. For example, to measure retrieval latency:


import time

start_time = time.time()
# Assuming vectorstore is already initialized
retrieved_docs = vectorstore.similarity_search("your query", k=3)
end_time = time.time()
retrieval_latency = end_time - start_time
print(f"Retrieval latency: {retrieval_latency:.4f} seconds")
print(f"Retrieved {len(retrieved_docs)} documents.")

Optimizations can include:

  • Hardware: Utilizing GPUs for both LLM inference (Ollama) and potentially embedding generation significantly boosts speed. For ChromaDB, sufficient RAM and fast SSDs are crucial.
  • Model Selection: Choosing smaller, quantized LLMs or embedding models if hardware is constrained. For example, using a Q4_K_M quantized Llama 3.1 8B model might offer a 5-10x speedup over a full 70B model on a consumer GPU, with a manageable quality trade-off.
  • ChromaDB Configuration: Tuning ChromaDB's collection settings, such as index type (e.g., HNSW) and `ef_construction` for faster indexing and search, though this can increase memory usage.
  • Chunking Strategy: Experimenting with different chunk sizes and overlaps can improve retrieval relevance.
  • Batching: If processing multiple queries, batching them for Ollama can improve throughput on GPUs.

Comparing hardware costs: a single NVIDIA RTX 3090 (24GB VRAM) can cost around $1000-$1500 used and can run quantized 70B models or full 13B models. A cloud GPU instance like AWS's g4dn.xlarge (16GB VRAM) costs about $0.53 per hour. Over a year, this can easily exceed the cost of dedicated hardware for many use cases.

Security and Maintenance Considerations

Running services locally introduces security considerations that differ from managed cloud services. Ensure your Docker containers are running with the least privileges necessary. If exposing these services outside your local network, robust security measures like firewalls, authentication, and HTTPS are paramount. Regularly update Docker images for Ollama, ChromaDB, and your application dependencies to patch security vulnerabilities. For Ollama, monitor its logs for any unusual activity. ChromaDB's data directory should be regularly backed up.

Maintenance also involves managing model updates. As new, improved open-source LLMs are released, you can easily pull them with Ollama and update your application configuration to use them. For example, switching from `llama3` to `llama3.1` might involve a simple change in the `Ollama` constructor: `llm = Ollama(model=”llama3.1″, base_url=”http://ollama:11434″)`. This agility is a significant advantage of a self-hosted approach. Keep an eye on ChromaDB updates as well, as performance and feature enhancements are continually being made. The community around these open-source projects is active, providing a good support channel for troubleshooting.

Conclusion and Next Steps

You've now got a blueprint for a powerful, self-hosted, open-source RAG LLM server. This setup offers unparalleled control, cost-efficiency, and data privacy for your AI-powered applications. By leveraging Docker, Ollama, ChromaDB, and LangChain, you've built a flexible system that can be adapted to a wide range of use cases, from internal knowledge bases to sophisticated Q&A bots. The ability to swap models, manage data locally, and optimize performance on your own hardware is a game-changer for builders and automators.

Here are three concrete action items to take this forward:

  1. Deploy the Docker Compose: Adapt the provided `docker-compose.yml` to your environment and get the core services running.
  2. Ingest Your Data: Implement a data ingestion pipeline using LangChain's loaders and splitters to populate your ChromaDB instance with your specific documents.
  3. Benchmark and Iterate: Measure the performance of your setup with your chosen LLM and embedding model, and experiment with optimizations like quantization or different chunking strategies.

My specific recommendation is to start with a smaller, quantized model like `llama3:8b-instruct-q4_K_M` on Ollama to get faster initial results and then scale up to larger models like `llama3.1:70b` as your hardware and needs dictate. This iterative approach will help you fine-tune your RAG pipeline for optimal performance and cost-effectiveness.

Frequently Asked Questions

What are the hardware requirements for self-hosting a RAG LLM server?

The hardware requirements vary significantly based on the LLM and embedding model you choose, as well as the volume of data. For running a 70B parameter LLM like Llama 3.1 70B, a GPU with at least 48GB of VRAM is generally recommended for reasonable performance, though quantized versions can run on GPUs with 24GB or even 16GB. For ChromaDB, sufficient RAM (16GB+) and fast SSD storage are crucial for efficient indexing and querying, especially with millions of embeddings. A CPU with a good core count also helps with general processing and data ingestion. For basic RAG with smaller models (e.g., 7B or 13B parameters) and moderate data, a system with a mid-range GPU (12GB+ VRAM), 32GB RAM, and an SSD might suffice.

How do I secure my self-hosted RAG server?

Securing your self-hosted RAG server involves several layers of protection. If running within a private network, ensure network access is strictly controlled. If exposing services externally, implement robust firewall rules, use HTTPS for all API endpoints (e.g., via a reverse proxy like Nginx or Traefik), and consider API key authentication or other authorization mechanisms for your application's endpoints. Regularly update all software components (Docker images, host OS, libraries) to patch known vulnerabilities. Avoid running containers with root privileges and limit their access to host resources. For sensitive data, consider encrypting the data at rest in ChromaDB's data directory.

Can I use other vector databases besides ChromaDB?

Absolutely. While ChromaDB is excellent for its ease of use and integration within the Python ecosystem, you can substitute it with other powerful vector databases. Popular alternatives include Pinecone (managed service), Weaviate (open-source, self-hostable), Milvus (open-source, self-hostable), and Qdrant (open-source, self-hostable). The integration process would involve replacing the `langchain-chroma` components with the corresponding LangChain integrations for your chosen database (e.g., `langchain-weaviate`, `langchain-milvus`). Each database has its own strengths in terms of scalability, performance, deployment complexity, and feature set, so the choice often depends on your specific project requirements and operational expertise.


Get the AI Edge, Weekly

The tools, tutorials, and trends that actually pay — no hype.

Featured on
Listed on DevTool.io Listed on SaaSHub
Scroll to Top