How to Install Weaviate on Ubuntu 24.04 VPS: Open-Source Vector Database with Hybrid Search
Weaviate is one of the most powerful open-source vector databases available today, combining semantic vector search with traditional keyword (BM25) search in a single engine. If you are building Retrieval-Augmented Generation (RAG) pipelines, AI-powered search, recommendation systems, or any application that needs to find information by meaning rather than exact keywords, Weaviate gives you a production-grade foundation you can run entirely on your own VPS. This tutorial walks you through deploying Weaviate on Ubuntu 24.04 using Docker Compose, configuring optional AI modules, designing collections, running hybrid queries, setting up S3 backups, and placing the whole thing behind an Nginx reverse proxy with TLS.
Prefer a different vector database? See our guides for Qdrant and Milvus. If you want to run LLMs locally to pair with Weaviate, follow our Ollama install guide.
Table of Contents
What is Weaviate?
Weaviate is an open-source, AI-native vector database written in Go. Unlike traditional databases that index rows by primary key or full-text token, Weaviate stores vector embeddings -- dense numerical representations of text, images, or other data -- and indexes them using Hierarchical Navigable Small World (HNSW) graphs. This lets you ask questions like "find me documents similar in meaning to this query" in milliseconds, even across hundreds of millions of records.
What sets Weaviate apart from other vector databases is its modular architecture. Out of the box, Weaviate can run as a pure vector store where you supply your own embeddings. But with its built-in module system, it can also vectorize your data automatically using transformer models (text2vec-transformers, text2vec-openai, text2vec-cohere), generate answers from retrieved context (generative-openai, generative-anthropic, generative-ollama), rerank results with cross-encoders (reranker-transformers, reranker-cohere), and even classify or summarize content. This "database + AI" design means you can build complete RAG pipelines without wiring together five separate services.
Weaviate supports both GraphQL and REST APIs, plus native gRPC for high-throughput clients. Its query language natively handles hybrid search, which combines sparse keyword scoring (BM25) with dense vector similarity and merges them using a configurable alpha weight. That single feature -- hybrid search done well -- is the reason many teams pick Weaviate over simpler vector-only stores. You get the recall of semantic search plus the precision of keyword matching in one query.
Common Weaviate use cases include semantic document search over wikis and knowledge bases, RAG backends for AI assistants and chatbots, product recommendations based on embedding similarity, image search using CLIP or similar multimodal models, duplicate detection in large catalogs, customer support triage matching new tickets to past resolutions, and e-commerce search that understands "comfy running shoes for wide feet" without requiring those exact keywords to appear in the product description.
Why Self-Host Weaviate on Your VPS?
Managed vector database services charge per dimension, per vector, and per query. Costs escalate quickly once you cross a few million records. Running Weaviate on your own VPS gives you several concrete advantages:
- Flat, predictable pricing -- A single VPS runs your entire Weaviate deployment regardless of vector count. No per-query, per-vector, or per-GB fees.
- Data sovereignty -- Your embeddings stay on your server. For teams handling proprietary documents, customer data, or regulated content (GDPR, HIPAA), self-hosting eliminates the third-party data-processing agreement problem.
- No vendor lock-in -- Weaviate is Apache 2.0 licensed. The same Docker image runs on any infrastructure. Switch providers, move to bare metal, or migrate to Kubernetes without rewriting a line of application code.
- Full module control -- On managed Weaviate Cloud you are limited to the modules the provider enables. Self-hosted, you can plug in any embedding model, any LLM (including local ones via
generative-ollama), or custom Python inference containers. - Tight coupling with other self-hosted AI -- If you already run Ollama, Llama.cpp, or Hugging Face TGI on the same server, Weaviate can call them over localhost with zero egress cost and sub-millisecond latency.
- Unlimited experimentation -- Spin up multiple collections, test different vectorizers, reindex on a whim. No cost anxiety.
Cost Comparison: Weaviate Cloud vs. Self-Hosted
| Scenario | Weaviate Cloud Serverless | Pinecone Standard | Self-Hosted Weaviate (VPS) |
|---|---|---|---|
| Monthly cost (10M vectors, 768 dims) | ~$295/mo | ~$350/mo | EUR 19.99/mo (flat) |
| Included storage | Metered | Metered | 100 GB NVMe |
| Included queries | Metered | Metered | Unlimited |
| Data leaves your infrastructure? | Yes | Yes | No |
| Custom modules | Limited | No | Unlimited |
| Hybrid search | Yes | Limited | Yes (first-class) |
| Backup flexibility | Vendor format | Vendor format | S3, filesystem, GCS |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to the server
- At least 8 GB of RAM (12 GB+ recommended if you plan to enable the
text2vec-transformersmodule, which loads a sentence-transformer model in-process) - At least 40 GB of free disk space -- Weaviate stores HNSW graphs and object data on disk; one million 768-dimension vectors takes roughly 3-5 GB with replication overhead
- A domain name pointed at your server's public IP (required for Nginx + Let's Encrypt TLS)
Recommended Plan: CloudCore Professional>
For a production Weaviate deployment serving millions of vectors with hybrid search and an on-device vectorizer module, we recommend the CloudCore Professional plan:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This gives you enough headroom for Weaviate itself (~2 GB baseline), a transformers inference container (~4-6 GB), HNSW in-memory indexes, and OS overhead. For larger deployments (50M+ vectors or very high QPS), scale up to a 32 GB plan or run Weaviate in clustered mode across multiple VPS instances.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
Start with a fresh package index and apply any pending security updates. This ensures your kernel and libraries are current before you install Docker.
sudo apt update && sudo apt upgrade -yIf a new kernel was installed, reboot and reconnect:
sudo rebootInstall a few baseline utilities that you will use later:
sudo apt install -y curl ca-certificates gnupg ufw jqStep 2: Install Docker and Docker Compose
Weaviate's recommended deployment path is Docker Compose. Install the official Docker Engine and Compose plugin from Docker's own apt repository (the version in Ubuntu's default repos is often outdated).
Add Docker's official GPG key and repository:
sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc sudo chmod a+r /etc/apt/keyrings/docker.ascecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
Install Docker Engine, the CLI, and the Compose plugin:
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginVerify both are installed:
docker --version
docker compose versionExpected output:
Docker version 27.5.0, build a187fa5
Docker Compose version v2.32.4Enable the Docker daemon to start on boot:
sudo systemctl enable --now dockerStep 3: Create the Weaviate Docker Compose File
Create a directory to hold your Weaviate deployment and its persistent data:
sudo mkdir -p /opt/weaviate
sudo mkdir -p /var/lib/weaviate/data
cd /opt/weaviateWeaviate uses a docker-compose.yml file to wire together the core database container and any optional module containers. Below is a production-ready Compose file that enables three commonly used modules:
text2vec-transformers-- Automatic vectorization using a sentence-transformer model (no external API calls)generative-openai-- Hooks Weaviate results directly into OpenAI completions for single-query RAGreranker-transformers-- Cross-encoder reranking for higher-precision result sets
sudo nano /opt/weaviate/docker-compose.ymlPaste the following:
services: weaviate: image: cr.weaviate.io/semitechnologies/weaviate:1.28.2 container_name: weaviate restart: unless-stopped ports: - "8080:8080" - "50051:50051" volumes: - /var/lib/weaviate/data:/var/lib/weaviate environment: QUERY_DEFAULTS_LIMIT: 25 AUTHENTICATION_APIKEY_ENABLED: "true" AUTHENTICATION_APIKEY_ALLOWED_KEYS: "${WEAVIATE_API_KEY}" AUTHENTICATION_APIKEY_USERS: "[email protected]" AUTHORIZATION_ADMINLIST_ENABLED: "true" AUTHORIZATION_ADMINLIST_USERS: "[email protected]" PERSISTENCE_DATA_PATH: "/var/lib/weaviate" DEFAULT_VECTORIZER_MODULE: "text2vec-transformers" ENABLE_MODULES: "text2vec-transformers,generative-openai,reranker-transformers,backup-s3" TRANSFORMERS_INFERENCE_API: "http://t2v-transformers:8080" RERANKER_INFERENCE_API: "http://reranker-transformers:8080" BACKUP_S3_BUCKET: "${BACKUP_S3_BUCKET}" BACKUP_S3_ENDPOINT: "${BACKUP_S3_ENDPOINT}" BACKUP_S3_USE_SSL: "true" AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID}" AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY}" CLUSTER_HOSTNAME: "node1" LOG_LEVEL: "info" depends_on: - t2v-transformers - reranker-transformers healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/v1/.well-known/ready"] interval: 15s timeout: 5s retries: 5t2v-transformers: image: cr.weaviate.io/semitechnologies/transformers-inference:sentence-transformers-all-MiniLM-L6-v2 container_name: weaviate-t2v restart: unless-stopped environment: ENABLE_CUDA: "0"
reranker-transformers: image: cr.weaviate.io/semitechnologies/reranker-transformers:cross-encoder-ms-marco-MiniLM-L-6-v2 container_name: weaviate-reranker restart: unless-stopped environment: ENABLE_CUDA: "0"
Save and exit (Ctrl+O, Enter, Ctrl+X).
A few notes on module choices:
all-MiniLM-L6-v2produces 384-dimension vectors. It is fast, small (~80 MB), and a strong default for general-purpose English semantic search. For better multilingual quality, swap toparaphrase-multilingual-MiniLM-L12-v2.cross-encoder-ms-marco-MiniLM-L-6-v2is the standard BEIR-benchmark reranker. It rescores the top-k results from a first-stage retrieval, pushing the best matches to the top.- If you do not need automatic vectorization, remove the
t2v-transformersservice and changeDEFAULT_VECTORIZER_MODULEtonone. You can then supply your own embeddings from OpenAI, Cohere, or any other source (see Step 7).
Step 4: Configure Environment Variables
Never hardcode API keys in docker-compose.yml. Create a .env file in the same directory:
sudo nano /opt/weaviate/.envGenerate a strong API key first. From another terminal:
openssl rand -hex 32Paste the output into the .env file along with your S3 credentials:
WEAVIATE_API_KEY=paste-the-generated-hex-string-here
BACKUP_S3_BUCKET=my-weaviate-backups
BACKUP_S3_ENDPOINT=s3.eu-central-1.amazonaws.com
AWS_ACCESS_KEY_ID=AKIAXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=your-secret-key-hereIf you do not plan to configure S3 backups yet, leave the S3 values blank -- the backup-s3 module only activates when you call the backup endpoint.
Lock down the permissions:
sudo chmod 600 /opt/weaviate/.envKey environment variables and what they do:
PERSISTENCE_DATA_PATH-- Where Weaviate stores HNSW indexes, inverted indexes, and object data. Map this to a host volume so your data survives container rebuilds.AUTHENTICATION_APIKEY_ENABLED-- Turns on API key auth. With this set totrue, all clients must send the key in theAuthorization: Bearer <key>header.AUTHENTICATION_APIKEY_ALLOWED_KEYS-- A comma-separated list of valid keys. You can issue multiple keys and rotate them by removing old ones.AUTHORIZATION_ADMINLIST_ENABLED/..._USERS-- Grants admin privileges (schema modification, deletion) to specific user identifiers.DEFAULT_VECTORIZER_MODULE-- Which module to use when a collection does not explicitly override it. Set tononeif you always supply your own vectors.ENABLE_MODULES-- Comma-separated list of modules to load at startup.QUERY_DEFAULTS_LIMIT-- Defaultlimitvalue when a query does not specify one. Prevents accidental full-table scans.
Step 5: Launch Weaviate
With the Compose file and environment in place, start the stack:
cd /opt/weaviate
sudo docker compose up -dOn first launch, Docker will pull three images totalling about 2.5 GB. This takes 1-4 minutes depending on bandwidth.
Check that all three containers are running:
sudo docker compose psExpected output:
NAME IMAGE STATUS PORTS
weaviate cr.weaviate.io/semitechnologies/weaviate:1.28.2 Up (healthy) 0.0.0.0:8080->8080/tcp, 0.0.0.0:50051->50051/tcp
weaviate-reranker cr.weaviate.io/semitechnologies/reranker-transformers:... Up
weaviate-t2v cr.weaviate.io/semitechnologies/transformers-inference:... UpTail the logs to verify everything started cleanly:
sudo docker compose logs -f weaviateLook for the line:
{"action":"startup","level":"info","msg":"Serving weaviate at http://[::]:8080"}Press Ctrl+C to exit the log stream.
Test the ready endpoint (this one does not require auth):
curl http://localhost:8080/v1/.well-known/readyExpected output: HTTP 200 with empty body.
Test an authenticated request:
source /opt/weaviate/.env
curl -H "Authorization: Bearer $WEAVIATE_API_KEY" \
http://localhost:8080/v1/meta | jq .Expected output:
{
"hostname": "http://[::]:8080",
"modules": {
"backup-s3": { "bucketName": "my-weaviate-backups" },
"generative-openai": { "documentationHref": "..." },
"reranker-transformers": { "documentationHref": "..." },
"text2vec-transformers": { "documentationHref": "..." }
},
"version": "1.28.2"
}Your Weaviate instance is up, authenticated, and knows about every module you enabled.
Step 6: Create Collections and Load Data
In Weaviate terminology, a collection (formerly "class") is like a table. It defines the property schema, the vectorizer, the distance metric, and the index parameters.
Create a collection called Article with three properties -- title, content, and category -- using the transformers vectorizer:
curl -X POST http://localhost:8080/v1/schema \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"class": "Article",
"description": "A knowledge-base article",
"vectorizer": "text2vec-transformers",
"moduleConfig": {
"text2vec-transformers": {
"poolingStrategy": "masked_mean",
"vectorizeClassName": false
},
"generative-openai": {
"model": "gpt-4o-mini"
},
"reranker-transformers": {}
},
"vectorIndexType": "hnsw",
"vectorIndexConfig": {
"distance": "cosine",
"ef": 128,
"efConstruction": 256,
"maxConnections": 32
},
"invertedIndexConfig": {
"bm25": {
"b": 0.75,
"k1": 1.2
}
},
"properties": [
{
"name": "title",
"dataType": ["text"],
"tokenization": "word",
"indexFilterable": true,
"indexSearchable": true
},
{
"name": "content",
"dataType": ["text"],
"tokenization": "word",
"indexFilterable": false,
"indexSearchable": true
},
{
"name": "category",
"dataType": ["text"],
"tokenization": "field",
"indexFilterable": true,
"indexSearchable": false
}
]
}'A few schema decisions worth understanding:
distance: cosineis the standard choice for sentence-transformer embeddings. Usedotfor some older models orl2-squaredfor image embeddings.efandefConstructiontrade recall for speed. Higher values give better recall but slower ingestion and slightly slower queries. The defaults above are a solid production starting point.- BM25
k1andbcontrol keyword scoring. The defaults match Elasticsearch's defaults. tokenization: fieldoncategorytreats the whole string as a single token -- perfect for filtering by exact category names.
curl -X POST http://localhost:8080/v1/batch/objects \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"objects": [
{
"class": "Article",
"properties": {
"title": "Installing Ollama on Ubuntu",
"content": "Ollama lets you run large language models locally on your VPS without any GPU required.",
"category": "ai"
}
},
{
"class": "Article",
"properties": {
"title": "Setting up Nginx as a reverse proxy",
"content": "Nginx can sit in front of any HTTP backend and terminate TLS using Let Encrypt certificates.",
"category": "networking"
}
},
{
"class": "Article",
"properties": {
"title": "Choosing the right VPS plan",
"content": "Pick RAM based on your largest workload, NVMe storage for database servers, and a provider with predictable monthly pricing.",
"category": "hosting"
}
}
]
}'Because the collection is configured with text2vec-transformers, Weaviate automatically sends each object to the inference container, receives a 384-dimension vector, and stores it in the HNSW index. No client-side embedding step required.
Step 7: BYO Vectors vs Module Vectorizer
You have two options for getting vectors into Weaviate.
Option A: Module Vectorizer (Automatic)
This is what you did in Step 6. Weaviate handles embedding generation internally by calling the configured inference container. Pros: simple, one moving part, vectorize at ingest and query time with a consistent model. Cons: tied to the model the container ships with, and inference adds CPU load to the database host.
Option B: Bring Your Own Vectors (BYO)
For tighter cost control, or if you want to use a larger/better model (OpenAI text-embedding-3-large, Cohere embed-v3, or a fine-tuned domain-specific model), compute embeddings in your application and hand Weaviate the vector directly.
First, create a collection with vectorizer: "none":
curl -X POST http://localhost:8080/v1/schema \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"class": "Document",
"vectorizer": "none",
"vectorIndexConfig": { "distance": "cosine" },
"properties": [
{ "name": "title", "dataType": ["text"] },
{ "name": "body", "dataType": ["text"] }
]
}'Then insert objects with an explicit vector array (here showing a simplified 4-dim vector; in practice you will use 384, 768, 1024, or 1536 dimensions depending on your embedding model):
curl -X POST http://localhost:8080/v1/objects \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"class": "Document",
"properties": {
"title": "Example doc",
"body": "The quick brown fox jumps over the lazy dog."
},
"vector": [0.12, -0.88, 0.45, 0.03]
}'At query time, compute the query vector in your application and pass it via nearVector:
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Document(nearVector: {vector: [0.11, -0.80, 0.50, 0.04]}, limit: 5) { title body _additional { distance } } } }"
}'Most teams end up with a mix: BYO for main content collections (using a production embedding model) and module vectorizers for admin or internal-only indexes.
Step 8: Query with GraphQL and REST
Weaviate offers two query surfaces. GraphQL is the richer, more expressive API and supports all features including hybrid search and generative modules. REST is simpler and good for CRUD operations.
GraphQL: Semantic search with nearText
Because the Article collection uses text2vec-transformers, you can query using natural language without computing embeddings yourself:
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Article(nearText: {concepts: [\"how do I run AI models on my own server\"]}, limit: 3) { title content category _additional { distance certainty } } } }"
}' | jq .Expected output:
{
"data": {
"Get": {
"Article": [
{
"title": "Installing Ollama on Ubuntu",
"content": "Ollama lets you run large language models locally on your VPS without any GPU required.",
"category": "ai",
"_additional": { "distance": 0.21, "certainty": 0.895 }
},
{
"title": "Choosing the right VPS plan",
"content": "...",
"category": "hosting",
"_additional": { "distance": 0.48, "certainty": 0.76 }
}
]
}
}
}Notice that the semantic search surfaced the Ollama article for a query that contained none of its keywords.
REST: Fetch by ID
curl -H "Authorization: Bearer $WEAVIATE_API_KEY" \
"http://localhost:8080/v1/objects/Article/UUID-HERE"REST: List all objects in a collection
curl -H "Authorization: Bearer $WEAVIATE_API_KEY" \
"http://localhost:8080/v1/objects?class=Article&limit=10"GraphQL: Generative RAG in one request
With the generative-openai module you can retrieve and generate in a single round trip. Supply your OpenAI key via an extra header:
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "X-OpenAI-Api-Key: sk-..." \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Article(nearText: {concepts: [\"running LLMs locally\"]}, limit: 3) { title _additional { generate(singleResult: {prompt: \"Summarize this article in one sentence: {title} -- {content}\"}) { singleResult } } } } }"
}'Weaviate performs the vector search, feeds each result into your prompt template, calls OpenAI, and returns the generated text alongside the source documents. This is the shortest possible path to a working RAG system.
Step 9: Hybrid Search (BM25 + Vector)
Semantic vector search is excellent at understanding meaning but can miss exact matches (product codes, part numbers, proper nouns). BM25 keyword search is excellent at precision but misses paraphrases. Hybrid search combines both in a single query and merges the rankings.
Weaviate's hybrid query runs both searches, normalizes the scores, and combines them using an alpha weight where alpha: 1.0 is pure vector, alpha: 0.0 is pure BM25, and alpha: 0.5 is an equal mix.
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Article(hybrid: {query: \"Nginx TLS setup\", alpha: 0.5}, limit: 5) { title content category _additional { score explainScore } } } }"
}' | jq .The _additional { score explainScore } fields let you inspect how each result ranked and why. A typical response shows the Nginx article at the top (strong BM25 match on "Nginx" plus strong vector similarity on "TLS") followed by progressively less-relevant matches.
Tuning alpha by use case
alpha: 0.75-- Favor semantic matching. Use for Q&A systems and knowledge-base search where users phrase queries naturally.alpha: 0.5-- Balanced. Good default for general-purpose search.alpha: 0.25-- Favor keyword matching. Use for product catalogs, code search, or any domain with lots of specific terminology.alpha: 0.0-- Pure BM25. Falls back to behavior similar to Elasticsearch.
Adding a reranker for even higher precision
Chain the reranker module on top of the hybrid results to get the best of three worlds:
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Article(hybrid: {query: \"how to run LLMs on a VPS without GPU\", alpha: 0.6}, limit: 20) { title content _additional { rerank(property: \"content\", query: \"how to run LLMs on a VPS without GPU\") { score } } } } }"
}'The reranker takes the top 20 hybrid results, rescores them with a cross-encoder (which reads the query and document together, unlike the bi-encoder used for first-stage retrieval), and returns them in the new order. This pattern -- cheap first-stage retrieval of 20-50 candidates followed by expensive reranking of that smaller set -- is the gold standard for RAG pipelines.
Step 10: Filters and Where Clauses
Hybrid search is powerful, but real applications need metadata filtering: "find articles similar to X, but only in the ai category and published after 2024". Weaviate's where clause handles this.
Exact match filter
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Article(nearText: {concepts: [\"reverse proxy\"]}, where: {path: [\"category\"], operator: Equal, valueText: \"networking\"}, limit: 5) { title category } } }"
}'Compound filter with AND
curl -X POST http://localhost:8080/v1/graphql \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"query": "{ Get { Article(hybrid: {query: \"ollama\", alpha: 0.5}, where: {operator: And, operands: [{path: [\"category\"], operator: Equal, valueText: \"ai\"}, {path: [\"title\"], operator: Like, valueText: \"Ubuntu\"}]}, limit: 10) { title category } } }"
}'Supported operators
Equal,NotEqual,GreaterThan,GreaterThanEqual,LessThan,LessThanEqualLikewith*wildcards for textContainsAny,ContainsAllfor array fieldsIsNullfor missing valuesWithinGeoRangefor geospatial coordinates
Step 11: Back Up Weaviate to S3
Data loss is not a theoretical risk. Weaviate ships with a first-class backup system that snapshots HNSW indexes and objects atomically to S3, GCS, Azure Blob, or local filesystem. You already enabled backup-s3 in Step 3.
Create an S3 bucket
Using the AWS CLI (or your provider's console), create a bucket in the region you set in BACKUP_S3_ENDPOINT:
aws s3 mb s3://my-weaviate-backups --region eu-central-1Enable versioning so accidental deletions are recoverable:
aws s3api put-bucket-versioning \
--bucket my-weaviate-backups \
--versioning-configuration Status=EnabledTrigger a backup
curl -X POST http://localhost:8080/v1/backups/s3 \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"id": "backup-2026-04-16-daily",
"include": ["Article", "Document"]
}'Check status:
curl -H "Authorization: Bearer $WEAVIATE_API_KEY" \
http://localhost:8080/v1/backups/s3/backup-2026-04-16-daily | jq .Expected output when complete:
{
"id": "backup-2026-04-16-daily",
"path": "s3://my-weaviate-backups/backup-2026-04-16-daily",
"status": "SUCCESS"
}Restore from a backup
curl -X POST http://localhost:8080/v1/backups/s3/backup-2026-04-16-daily/restore \
-H "Authorization: Bearer $WEAVIATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"include": ["Article"]}'Automate daily backups
Create a cron entry that hits the backup endpoint every night:
sudo crontab -eAdd:
0 3 * curl -s -X POST http://localhost:8080/v1/backups/s3 -H "Authorization: Bearer $(grep WEAVIATE_API_KEY /opt/weaviate/.env | cut -d= -f2)" -H "Content-Type: application/json" -d "{\"id\": \"backup-$(date +\%Y\%m\%d)\"}" >> /var/log/weaviate-backup.log 2>&1Pair this with an S3 lifecycle policy that expires backups older than 30 days to keep storage costs bounded.
Step 12: Secure Weaviate with Nginx and TLS
Weaviate's port 8080 is currently bound to all interfaces. Put Nginx in front of it with Let's Encrypt TLS so clients connect over HTTPS.
First, restrict Weaviate's port so it is only reachable via Nginx:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw deny 8080/tcp
sudo ufw deny 50051/tcp
sudo ufw enableInstall Nginx and Certbot:
sudo apt install -y nginx certbot python3-certbot-nginxCreate the server block:
sudo nano /etc/nginx/sites-available/weaviatePaste:
server { listen 80; server_name weaviate.yourdomain.com; return 301 https://$host$request_uri; }server { listen 443 ssl http2; server_name weaviate.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/weaviate.yourdomain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/weaviate.yourdomain.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always;
client_max_body_size 50m;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off; proxy_read_timeout 300s; proxy_send_timeout 300s; }
location = /v1/.well-known/live { proxy_pass http://127.0.0.1:8080; access_log off; } }
Enable and reload:
sudo ln -s /etc/nginx/sites-available/weaviate /etc/nginx/sites-enabled/
sudo nginx -t
sudo certbot --nginx -d weaviate.yourdomain.com
sudo systemctl reload nginxCertbot sets up auto-renewal via systemd timer. Test with:
sudo certbot renew --dry-runNow every request hits https://weaviate.yourdomain.com and still requires the Bearer token. For gRPC on port 50051, either tunnel through an SSH bastion or use Nginx with stream blocks -- but most HTTP-based clients (Python, JS, Go) work perfectly over the REST/GraphQL path.
Performance Tuning
HNSW parameters
The most impactful tuning levers live in the collection's vectorIndexConfig:
ef(query time) -- Higher values search more nodes per query, improving recall but increasing latency. Typical range: 64-512. Start at 128.efConstruction(build time) -- Higher values build a denser graph with better recall but slower ingestion. Typical range: 128-512. Start at 256.maxConnections-- Number of outgoing links per node. Higher improves recall at the cost of memory. Typical range: 16-64. Start at 32.
ef on a live collection without reindexing; efConstruction and maxConnections only apply to new vectors.Memory sizing
Weaviate keeps the HNSW graph in memory for query speed. Rough rule: plan for vectors dimensions 4 bytes * 1.5 RAM. For 10 million 768-dimension vectors that is roughly 46 GB. Use product quantization (PQ) to compress vectors in memory at a small recall cost:
{
"vectorIndexConfig": {
"pq": { "enabled": true, "segments": 96, "trainingLimit": 100000 }
}
}PQ typically reduces memory by 8-16x with under 2% recall loss.
Shard count
For single-node deployments, one shard is fine. When scaling beyond ~50M vectors, switch to multi-node and configure shards per collection to spread load across nodes.
Batch ingestion
Ingesting with the /v1/batch/objects endpoint is 10-50x faster than one-at-a-time /v1/objects calls. Use batches of 100-500 objects per request.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
401 Unauthorized on every request | Missing or wrong Bearer token | Verify Authorization: Bearer $WEAVIATE_API_KEY header. Check the key in /opt/weaviate/.env matches the one Docker loaded: sudo docker exec weaviate env \</td><td>grep AUTHENTICATION. |
context deadline exceeded on ingest | Transformers container still loading model | Tail logs: sudo docker compose logs t2v-transformers. Wait until you see Application startup complete. First-time startup takes 30-90s. |
Collection creation fails with module not enabled | Module name typo in ENABLE_MODULES | Check spelling against Weaviate docs. Restart stack after editing: sudo docker compose up -d. |
| Queries return zero results despite data present | Collection vectorizer mismatch | Inspect the class config: curl -H "Authorization: Bearer $KEY" http://localhost:8080/v1/schema/Article. Ensure vectorizer matches how data was ingested. |
| Out-of-memory kills | HNSW graph exceeds RAM | Enable PQ compression, reduce maxConnections, or move to a larger plan. Swap file helps as a stopgap: sudo fallocate -l 8G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile. |
Backup fails with NoSuchBucket | S3 bucket missing or wrong region | Create the bucket and verify BACKUP_S3_ENDPOINT matches the region. Keys need s3:PutObject, s3:GetObject, s3:ListBucket. |
docker compose command not found | Old Docker Compose v1 installed | Install Compose v2 plugin: sudo apt install docker-compose-plugin. |
| Slow BM25 scoring on large text fields | Inverted index not built | Verify indexSearchable: true on the text property. Reindexing requires recreating the collection. |
sudo docker compose -f /opt/weaviate/docker-compose.yml logs -f --tail=100FAQ
How does Weaviate compare to Qdrant and Milvus?
Weaviate is the most feature-rich of the three, with first-class modules for vectorization, generative AI, and reranking. It shines when you want a single system that handles the full RAG pipeline. Its hybrid search implementation is among the best available.
Qdrant is faster for pure vector search and has a simpler operational footprint (no module containers). It is an excellent choice when you compute embeddings in your application and just need a lightning-fast index. See our Qdrant install guide.
Milvus is the heavyweight enterprise option, designed for multi-billion-vector deployments with clustered storage backends (MinIO, etcd, Pulsar). Higher operational complexity but scales further than either Weaviate or Qdrant. See our Milvus install guide.
Pick Weaviate when you want hybrid search, modular AI integrations, and a developer-friendly GraphQL API.
Does Weaviate work without a GPU?
Yes. All examples in this guide run entirely on CPU. The text2vec-transformers and reranker-transformers containers use CPU inference by default (ENABLE_CUDA: "0"). A 6 vCPU VPS comfortably ingests a few hundred documents per second with the all-MiniLM-L6-v2 model. For high-throughput production workloads (>1000 docs/sec ingest) or larger embedding models (sentence-transformers v2 XL), a GPU plan helps.
Can I run Weaviate clustered across multiple VPS?
Yes. Weaviate supports native clustering via Raft for schema replication and gossip for node discovery. You set CLUSTER_HOSTNAME and CLUSTER_JOIN environment variables on each node. Most single-tenant deployments (even large ones) run single-node until they hit roughly 50M vectors or need fault tolerance. When you cross that threshold, plan a weekend to migrate to a 3-node cluster with shard replication factor 2 or 3.
How do I migrate data from Pinecone or Weaviate Cloud to self-hosted?
Pinecone exposes a fetch API that returns vectors and metadata; write a Python script that pages through your namespace and batches into Weaviate's /v1/batch/objects endpoint. From Weaviate Cloud, use the built-in backup module to export to an S3 bucket, then restore from that same bucket into your self-hosted node -- the data format is identical. Typical migration for 10M vectors takes 1-4 hours on a CloudCore Professional VPS.
What is the right way to use hybrid search alpha?
Start at alpha: 0.5 and measure. Build a small evaluation set of query/expected-result pairs and sweep alpha from 0.0 to 1.0 in 0.1 steps, recording MRR (mean reciprocal rank) or nDCG at each value. Pick the alpha that maximizes your chosen metric. For most knowledge-base and documentation use cases, the best alpha lands between 0.55 and 0.75. For code search and product catalogs with specific identifiers, it is usually 0.25 to 0.45.
Can Weaviate handle image or multimodal vectors?
Yes. Use the multi2vec-clip module to embed images and text into the same 512-dim CLIP space, then query with nearImage or nearText against the same collection. Alternatively, compute CLIP embeddings in your application and use BYO vectors with vectorizer: "none". Multimodal search is a great fit for e-commerce, media libraries, and content moderation pipelines.
Next Steps
Now that Weaviate is running on your VPS, here are practical next steps.
- Plug in a local LLM for end-to-end private RAG -- Combine Weaviate with Ollama and the
generative-ollamamodule to build a RAG pipeline where no data leaves your server. Ask questions of your knowledge base and get answers from a local Llama 3.1 or Mistral model. - Compare alternative vector stores -- For pure speed on small-to-medium datasets, benchmark against Qdrant. For massive scale (100M+ vectors), evaluate Milvus.
- Add a query layer -- Install the Weaviate Python client (
pip install -U weaviate-client) or the TypeScript client and start building your application against a typed SDK instead of raw curl. - Set up monitoring -- Weaviate exposes Prometheus metrics at
/v1/metrics. Scrape them with your monitoring stack and alert onweaviate_objects_durations_msandweaviate_queries_durations_mspercentiles. - Read the official docs -- The Weaviate documentation covers advanced topics like multi-tenancy, cross-references between collections, named vectors, and replication.
Ready to deploy?>
Weaviate runs beautifully on a CloudCore Professional VPS -- 6 vCPU, 12 GB RAM, 100 GB NVMe, unmetered bandwidth, EUR 19.99/month. Enough headroom for tens of millions of vectors, the transformers module, and your application stack on a single server.>
Deploy your Weaviate VPS now.