How to Install Milvus on Ubuntu 24.04 VPS: Scalable Open-Source Vector Database
Vector search powers the modern AI stack. Semantic search, retrieval-augmented generation (RAG), recommendation engines, image similarity, fraud detection, and agent memory all rely on the same primitive: store high-dimensional embedding vectors and retrieve the nearest neighbors to a query in milliseconds. Milvus is the open-source vector database that has become the de facto standard for workloads that start small and need to scale to billions of vectors without re-architecting.
This guide walks you through installing Milvus Standalone on an Ubuntu 24.04 VPS with Docker Compose, defining a production collection schema, building HNSW and IVF_FLAT indexes, running similarity search, combining dense vectors with BM25 lexical search for hybrid retrieval, organizing data with partitions, browsing everything in the Attu web UI, and configuring backups. By the end you will have a production-ready vector database that can back a RAG pipeline, a semantic search API, or any embedding-driven application.
Prefer a turnkey build? Our CloudCore Professional plan gives you the 12 GB of RAM Milvus needs to comfortably serve several million vectors with HNSW indexing for EUR 19.99/month.
Table of Contents
What is Milvus?
Milvus is an open-source vector database designed from the ground up for similarity search over embedding vectors. It was first released in 2019 by Zilliz, donated to the LF AI & Data Foundation in 2020, and is now a graduated project with contributors from across the AI industry. At its core Milvus solves one problem extremely well: given a query vector, return the k nearest vectors out of a collection that may contain hundreds of millions or billions of entries, in sub-100-millisecond latency.
Unlike general-purpose databases that bolt on a vector extension (pgvector on PostgreSQL, for example), Milvus was architected around approximate nearest neighbor (ANN) indexing from day one. It supports a full menu of indexes - HNSW (graph-based, best default), IVF_FLAT and IVF_SQ8 (inverted file with optional scalar quantization), IVF_PQ (product quantization for billion-scale), DISKANN (SSD-resident for datasets bigger than RAM), GPU_CAGRA and GPU_IVF_FLAT (CUDA-accelerated) - plus metric types covering L2, inner product, cosine similarity, and Hamming distance.
Milvus is not just an index. It provides a collection abstraction with strongly-typed fields, partitions for logical data isolation, scalar filtering combined with vector search, time-travel via multi-version concurrency control, dynamic schema fields, sparse vector support for BM25 and SPLADE, and since version 2.5 a built-in BM25 function that replaces the need for a separate text search service like Elasticsearch for many workloads.
The practical use cases are broad. Engineering teams use Milvus to power RAG pipelines feeding Ollama, vLLM, and OpenAI-compatible LLMs with retrieved document chunks. E-commerce companies use it for visual product search over image embeddings. Music and video platforms use it for audio fingerprinting and recommendation. Security teams use it for malware classification via binary embeddings. Research labs use it for scientific paper discovery via citation and text embeddings. If your application needs "find things that are similar to this," Milvus is the workhorse.
Why Self-Host Milvus Instead of Using Zilliz Cloud?
Zilliz Cloud is the commercial managed Milvus service run by the company that created it. It is a fine product, but self-hosting on your own VPS has several concrete advantages, especially for teams early in their journey or with predictable workloads.
- Flat, predictable cost. Zilliz Cloud bills by compute unit, storage, and egress. A small RAG workload that keeps 2 million vectors loaded can easily run USD 150-400/month on managed services. The same workload fits comfortably on a CloudCore Professional VPS at EUR 19.99/month.
- Your embeddings never leave your infrastructure. Embeddings reconstruct a significant fraction of the underlying content. If you are indexing customer documents, internal code, legal contracts, or medical records, keeping the vector index on your own server (alongside the LLM and the source documents) simplifies compliance and eliminates one more data-processing agreement.
- No egress fees. Cloud providers charge to send data out. On your own VPS every byte of query results is free.
- Full control over configuration. Tune index parameters, enable experimental features, pin to a specific Milvus version, run custom plugins, or downgrade when a new release regresses on your workload. Managed services intentionally restrict these knobs.
- Co-locate with your LLM. If you are running Ollama or vLLM on the same network for generation, placing Milvus next to it removes network round-trips and slashes end-to-end RAG latency by 20-80 ms per query.
- Run anywhere. Self-hosted Milvus runs on any Linux VPS, on-prem, on air-gapped networks, in any jurisdiction. Zilliz Cloud is restricted to the regions its operator supports.
- Open-source license. Milvus is Apache 2.0. You can fork it, audit it, or embed it without commercial restrictions.
Cost Comparison at 2 Million 768-dim Vectors
| Dimension | Zilliz Cloud (Serverless) | Zilliz Cloud (Dedicated) | Self-Hosted Milvus on VPS |
|---|---|---|---|
| Monthly cost | ~USD 60-120 | ~USD 300-500 | EUR 19.99 (CloudCore Professional) |
| Vectors loaded | 2M (768-dim) | 2M (768-dim) | 2M (768-dim) |
| Query latency | 20-50 ms | 10-30 ms | 15-40 ms (same-VPS) |
| Data sovereignty | Vendor region | Vendor region | Your server |
| Egress fees | Yes | Yes | None |
| Hybrid BM25 search | Yes | Yes | Yes |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- At least 8 GB RAM (16 GB recommended for production and larger indexes)
- At least 50 GB SSD for Milvus, MinIO object storage, and etcd metadata
- Docker Engine 24+ and the Docker Compose plugin (installed in Step 1)
- Python 3.10+ on the server or your workstation to run pymilvus examples
- A domain name pointed at the VPS if you want to expose Attu or the gRPC endpoint externally
Recommended Plan: CloudCore Professional>
For Milvus Standalone serving a few million vectors with HNSW, we recommend CloudCore Professional:>
- 6 vCPU cores
- 12 GB RAM
- 100 GB NVMe SSD
- Unmetered bandwidth
- EUR 19.99/month>
This sizes comfortably for 5M+ 768-dimensional embeddings with HNSW, scalar filtering, and an application process on the same box. For billion-scale workloads, step up to the 32 GB and 64 GB plans.
Connect to your server:
ssh root@your-server-ipStep 1: Update the System and Install Docker
Update packages and reboot if a new kernel was installed:
sudo apt update && sudo apt upgrade -yInstall Docker Engine and the Compose plugin from Docker's official repository:
sudo apt install -y ca-certificates curl gnupg lsb-release sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpgecho "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] 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 sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Verify the install:
docker --version
docker compose versionExpected output:
Docker version 27.3.1, build ce12230
Docker Compose version v2.29.7Add your user to the docker group so you can run commands without sudo:
sudo usermod -aG docker $USER
newgrp dockerIf you need a more detailed walkthrough of Docker itself, see our Docker install guide.
Step 2: Deploy Milvus Standalone with Docker Compose
Milvus Standalone is a single binary that embeds the query, data, and index nodes into one process, but it still depends on two external components: etcd for metadata storage and MinIO (or any S3-compatible object store) for persisting segments. Docker Compose is the cleanest way to run all three.
Create a working directory:
sudo mkdir -p /opt/milvus
cd /opt/milvusDownload the official docker-compose.yml file for the latest stable release:
sudo curl -sfL https://raw.githubusercontent.com/milvus-io/milvus/master/deployments/docker/standalone/docker-compose.yml -o docker-compose.ymlInspect the file. It provisions three services:
etcd(bitnami/etcd image) on port 2379 for metadataminio(minio/minio image) on ports 9000/9001 for object storagestandalone(milvusdb/milvus image) on port 19530 for gRPC and 9091 for the HTTP health endpoint
sudo docker compose up -dExpected output (abbreviated):
[+] Running 4/4
✔ Network milvus Created
✔ Container milvus-etcd Started
✔ Container milvus-minio Started
✔ Container milvus-standalone StartedWait for the health check to pass (typically 30-60 seconds on a fresh install while etcd initializes):
sudo docker compose psExpected output:
NAME IMAGE STATUS PORTS
milvus-etcd quay.io/coreos/etcd:v3.5.16 Up 45 seconds (healthy) 2379/tcp, 2380/tcp
milvus-minio minio/minio:RELEASE.2024-... Up 45 seconds (healthy) 9000-9001/tcp
milvus-standalone milvusdb/milvus:v2.5.4 Up 30 seconds (healthy) 0.0.0.0:9091->9091/tcp, 0.0.0.0:19530->19530/tcpConfirm the Milvus HTTP health endpoint:
curl http://localhost:9091/healthzExpected output:
OKMilvus is now running. By default it listens on 19530 for gRPC (the protocol pymilvus uses) and 9091 for HTTP metrics and health.
Bind Milvus to a Private Network
If your VPS has a private network interface, edit docker-compose.yml and change the standalone service's port mapping from "19530:19530" to "127.0.0.1:19530:19530" or to the private IP. For external access, always expose Milvus through a reverse proxy with TLS and authentication - never expose 19530 directly to the public internet.
Step 3: Install the pymilvus Client
Everything you do with Milvus goes through a client library. The official Python client is pymilvus, and it is what we will use throughout this guide. You can install it on the Milvus server itself or on a separate application box.
Install Python and create a virtualenv:
sudo apt install -y python3 python3-pip python3-venv
python3 -m venv ~/milvus-env
source ~/milvus-env/bin/activateInstall pymilvus and a few helpers we will use for embeddings:
pip install --upgrade pip
pip install "pymilvus>=2.5.0" "pymilvus[model]" numpyThe [model] extra installs sentence-transformers so you can generate real embeddings locally without an external API. Verify the client can connect:
# test_connection.py from pymilvus import MilvusClient
client = MilvusClient(uri="http://localhost:19530") print("Collections:", client.list_collections()) print("Connected:", client.get_server_version())
Run it:
python test_connection.pyExpected output:
Collections: []
Connected: v2.5.4Official clients are also available for Go, Java, Node.js, and a REST gateway. See the Milvus documentation for language-specific install instructions.
Step 4: Create a Collection with Vector and Scalar Fields
A collection in Milvus is analogous to a table in a relational database. Each row is an entity with a primary key, one or more vector fields, and any number of scalar fields that you can filter on.
The example below creates a documents collection suitable for a RAG pipeline. It stores a 384-dimensional embedding (the output of the all-MiniLM-L6-v2 sentence-transformer), plus the source text, a category tag, a timestamp, and a boolean flag.
# create_collection.py from pymilvus import MilvusClient, DataTypeclient = MilvusClient(uri="http://localhost:19530")
COLLECTION = "documents"
if client.has_collection(COLLECTION): client.drop_collection(COLLECTION)
schema = client.create_schema( auto_id=True, enable_dynamic_field=True, )
schema.add_field(field_name="id", datatype=DataType.INT64, is_primary=True) schema.add_field(field_name="embedding", datatype=DataType.FLOAT_VECTOR, dim=384) schema.add_field(field_name="text", datatype=DataType.VARCHAR, max_length=8192) schema.add_field(field_name="category", datatype=DataType.VARCHAR, max_length=64) schema.add_field(field_name="published_at", datatype=DataType.INT64) schema.add_field(field_name="is_public", datatype=DataType.BOOL)
client.create_collection( collection_name=COLLECTION, schema=schema, )
print("Created collection:", COLLECTION) print(client.describe_collection(COLLECTION))
Run it:
python create_collection.pyKey choices to understand:
auto_id=Truetells Milvus to generate the primary key automatically. Set it toFalseif you want to supply your own IDs (for example, to match rows in your source-of-truth database).enable_dynamic_field=Truelets you insert additional JSON properties at write time without altering the schema. This is useful when your metadata evolves rapidly.DataType.FLOAT_VECTORwithdim=384must exactly match the output dimensionality of your embedding model. For OpenAItext-embedding-3-smalluse 1536; for Cohereembed-v3use 1024; fornomic-embed-textuse 768.VARCHARwithmax_lengthis enforced at insert time. Pick a size that comfortably fits your longest chunk plus headroom.
INT8_VECTOR, FLOAT16_VECTOR, BFLOAT16_VECTOR, BINARY_VECTOR, and SPARSE_FLOAT_VECTOR (the last for BM25 and SPLADE-style lexical representations).Step 5: Build HNSW and IVF_FLAT Indexes
An unindexed collection still works - Milvus will do a brute-force scan - but latency scales linearly with dataset size. For anything bigger than a few thousand vectors you need an approximate nearest neighbor (ANN) index.
HNSW (Recommended Default)
HNSW (Hierarchical Navigable Small World) is a graph-based index that delivers the best recall-latency trade-off for most workloads. It is memory-hungry but fast.
# create_index_hnsw.py from pymilvus import MilvusClientclient = MilvusClient(uri="http://localhost:19530") COLLECTION = "documents"
index_params = client.prepare_index_params() index_params.add_index( field_name="embedding", index_type="HNSW", metric_type="COSINE", params={"M": 16, "efConstruction": 200}, )
client.create_index( collection_name=COLLECTION, index_params=index_params, sync=True, )
client.load_collection(COLLECTION) print("Index built and collection loaded into memory.")
Parameter tuning:
Mis the maximum number of outgoing edges per graph node. HigherM(32, 64) increases recall and memory; lowerM(8, 12) is faster to build but less accurate.16is the standard default.efConstructioncontrols build-time graph quality. Range 100-500. Higher means better recall at query time, at the cost of build speed.metric_typemust match how you computed your embeddings. Most modern sentence-transformers useCOSINE. OpenAI embeddings also use cosine. UseL2only if your model is explicitly trained for it.
ef (the search beam width). See Step 6.IVF_FLAT (Memory-Efficient Alternative)
IVF_FLAT partitions vectors into nlist clusters using k-means, then searches the nprobe closest clusters at query time. It uses less RAM than HNSW because it stores the raw vectors rather than a graph, and it is ideal for datasets that change frequently or need exact-within-cluster recall.
index_params = client.prepare_index_params()
index_params.add_index(
field_name="embedding",
index_type="IVF_FLAT",
metric_type="COSINE",
params={"nlist": 1024},
)
client.create_index(COLLECTION, index_params, sync=True)Rule of thumb for nlist: 4 * sqrt(N) where N is your dataset size. For 1 million vectors that is about 4000.
Index Comparison
| Index | Build Time | RAM Usage | Recall @ k=10 | Query Latency | Best For |
|---|---|---|---|---|---|
| FLAT | Instant | 1x raw | 100% (exact) | Slow at scale | < 100k vectors |
| HNSW | Slow | 2x raw | 98-99% | Fastest | General-purpose default |
| IVF_FLAT | Medium | 1x raw | 95-98% | Fast | Frequent updates |
| IVF_SQ8 | Medium | 0.25x raw | 90-95% | Fast | Memory-constrained |
| IVF_PQ | Slow | 0.1x raw | 85-92% | Fast | Billion-scale |
| DISKANN | Slow | 0.1x raw (RAM) | 95-97% | Medium | Larger-than-RAM |
Step 6: Insert Data and Run Similarity Search
Now generate some real embeddings and query them.
Generate Embeddings and Insert
# insert_data.py from pymilvus import MilvusClient from pymilvus.model.dense import SentenceTransformerEmbeddingFunction import timeclient = MilvusClient(uri="http://localhost:19530") COLLECTION = "documents"
embedder = SentenceTransformerEmbeddingFunction( model_name="all-MiniLM-L6-v2", device="cpu", )
docs = [ ("A VPS provides dedicated resources in a virtualized environment.", "hosting"), ("RAG combines retrieval with generative language models.", "ai"), ("Milvus is an open-source vector database for similarity search.", "ai"), ("HNSW graphs enable sub-millisecond nearest neighbor queries.", "ai"), ("Docker Compose orchestrates multi-container applications.", "devops"), ("UFW is a user-friendly front-end for iptables on Ubuntu.", "devops"), ("Cosine similarity measures the angle between two vectors.", "math"), ("Ollama runs large language models locally on CPU or GPU.", "ai"), ]
embeddings = embedder([d[0] for d in docs])
rows = [ { "embedding": embeddings[i].tolist(), "text": docs[i][0], "category": docs[i][1], "published_at": int(time.time()), "is_public": True, } for i in range(len(docs)) ]
result = client.insert(collection_name=COLLECTION, data=rows) print(f"Inserted {result['insert_count']} entities")
client.flush(collection_name=COLLECTION)
Run it:
python insert_data.pyRun a Top-k Similarity Search
# search.py from pymilvus import MilvusClient from pymilvus.model.dense import SentenceTransformerEmbeddingFunctionclient = MilvusClient(uri="http://localhost:19530") embedder = SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
query = "how to run language models on my own server" query_vec = embedder([query])[0].tolist()
results = client.search( collection_name="documents", data=[query_vec], limit=3, output_fields=["text", "category"], search_params={"metric_type": "COSINE", "params": {"ef": 64}}, )
for hit in results[0]: print(f"score={hit['distance']:.4f} [{hit['entity']['category']}] {hit['entity']['text']}")
Expected output:
score=0.7821 [ai] Ollama runs large language models locally on CPU or GPU.
score=0.5103 [ai] RAG combines retrieval with generative language models.
score=0.3415 [ai] Milvus is an open-source vector database for similarity search.Filtered Search (Scalar Expression)
Combine vector search with SQL-like scalar filters using the filter argument:
results = client.search(
collection_name="documents",
data=[query_vec],
limit=5,
filter='category == "ai" and is_public == True',
output_fields=["text", "category", "published_at"],
search_params={"metric_type": "COSINE", "params": {"ef": 64}},
)Supported expression operators include ==, !=, >, <, >=, <=, in, not in, like, and, or, not. This is what makes Milvus dramatically more useful than a raw ANN library: you get vector ranking and metadata filtering in one round-trip.
Step 7: Hybrid Search with BM25 (Milvus 2.4+)
Dense embeddings capture semantics but sometimes miss exact term matches ("MIL-STD-810", a product SKU, a function name). BM25 is the classic lexical scoring function that search engines have used for decades. Milvus 2.4 added native support for sparse vectors, and 2.5 added a built-in BM25 function that automatically tokenizes text and stores sparse term-frequency vectors - giving you true hybrid search in a single database.
Create a Collection with a BM25 Function
# hybrid_collection.py from pymilvus import MilvusClient, DataType, Function, FunctionTypeclient = MilvusClient(uri="http://localhost:19530") COLLECTION = "docs_hybrid"
if client.has_collection(COLLECTION): client.drop_collection(COLLECTION)
schema = client.create_schema(auto_id=True, enable_dynamic_field=True) schema.add_field("id", DataType.INT64, is_primary=True) schema.add_field("text", DataType.VARCHAR, max_length=8192, enable_analyzer=True) schema.add_field("dense", DataType.FLOAT_VECTOR, dim=384) schema.add_field("sparse", DataType.SPARSE_FLOAT_VECTOR)
bm25_fn = Function( name="bm25_fn", input_field_names=["text"], output_field_names=["sparse"], function_type=FunctionType.BM25, ) schema.add_function(bm25_fn)
client.create_collection(collection_name=COLLECTION, schema=schema)
Milvus now populates the sparse field automatically whenever you insert a text value. You do not tokenize on the client side.
Build Indexes for Both Fields
index_params = client.prepare_index_params()
index_params.add_index(
field_name="dense",
index_type="HNSW",
metric_type="COSINE",
params={"M": 16, "efConstruction": 200},
)
index_params.add_index(
field_name="sparse",
index_type="SPARSE_INVERTED_INDEX",
metric_type="BM25",
)
client.create_index(COLLECTION, index_params, sync=True)
client.load_collection(COLLECTION)Run Hybrid Search with RRF Reranking
# hybrid_search.py from pymilvus import MilvusClient, AnnSearchRequest, RRFRanker from pymilvus.model.dense import SentenceTransformerEmbeddingFunctionclient = MilvusClient(uri="http://localhost:19530") embedder = SentenceTransformerEmbeddingFunction(model_name="all-MiniLM-L6-v2")
query = "install milvus on ubuntu" query_vec = embedder([query])[0].tolist()
dense_req = AnnSearchRequest( data=[query_vec], anns_field="dense", param={"metric_type": "COSINE", "params": {"ef": 64}}, limit=20, )
sparse_req = AnnSearchRequest( data=[query], anns_field="sparse", param={"metric_type": "BM25"}, limit=20, )
results = client.hybrid_search( collection_name="docs_hybrid", reqs=[dense_req, sparse_req], ranker=RRFRanker(k=60), limit=10, output_fields=["text"], )
for hit in results[0]: print(f"{hit['distance']:.4f} {hit['entity']['text']}")
RRFRanker uses Reciprocal Rank Fusion, the standard combiner for heterogeneous result lists. For weighted blending use WeightedRanker(0.7, 0.3) to favor dense (semantic) or sparse (lexical) signal. Hybrid search typically improves recall by 5-15% over pure vector search on mixed-intent queries, especially those containing proper nouns, numeric identifiers, or rare terms.
Step 8: Organize Data with Partitions
Partitions are logical subdivisions of a collection. All entities share the same schema, but you can target queries at a specific partition, skipping every segment that does not belong to it. This is the right tool for per-tenant isolation, per-category sharding, or time-based data lifecycle.
# partitions.py
from pymilvus import MilvusClientclient = MilvusClient(uri="http://localhost:19530")
COLLECTION = "documents"
Create partitions for multi-tenant data
for tenant in ["tenant_a", "tenant_b", "tenant_c"]:
client.create_partition(COLLECTION, tenant)print(client.list_partitions(COLLECTION))
Insert into a specific partition
client.insert(
collection_name=COLLECTION,
partition_name="tenant_a",
data=[{
"embedding": [0.1] * 384,
"text": "Tenant A private document",
"category": "internal",
"published_at": 0,
"is_public": False,
}],
)Search within a partition (skips all other tenants entirely)
results = client.search(
collection_name=COLLECTION,
partition_names=["tenant_a"],
data=[[0.1] * 384],
limit=5,
output_fields=["text"],
)Partition queries are dramatically faster than filtered queries because they prune at the segment level, not at scan time. The default limit is 1024 partitions per collection, raisable via the maxPartitionNum config. For tens of thousands of tenants use the newer Partition Key feature, which lets you declare a field (for example tenant_id) as the partition key and have Milvus handle routing automatically.
Step 9: Install the Attu Web UI
Attu is the official graphical management UI for Milvus. It lets you browse collections, inspect schemas, visualize index stats, run vector queries from the browser, and monitor resource usage without writing any Python.
Append Attu to your Docker Compose stack:
sudo tee -a /opt/milvus/docker-compose.override.yml > /dev/null <<'EOF'
services:
attu:
container_name: milvus-attu
image: zilliz/attu:v2.5
environment:
MILVUS_URL: milvus-standalone:19530
ports:
- "127.0.0.1:3000:3000"
depends_on:
- standalone
networks:
- default
EOFStart Attu:
cd /opt/milvus
sudo docker compose up -d attuAttu now listens on 127.0.0.1:3000. Tunnel to it via SSH from your workstation:
ssh -L 3000:localhost:3000 root@your-server-ipOpen http://localhost:3000 in your browser, enter milvus-standalone:19530 as the Milvus address, and click Connect. You will see every collection, partition, and index you have created.
Expose Attu Publicly (Optional)
If you want to access Attu without an SSH tunnel, put it behind Nginx with SSL and HTTP basic auth, the same pattern described in our Ollama guide. Never expose Attu directly on the public internet without authentication.
Step 10: Backups and Disaster Recovery
The official backup tool is milvus-backup, which snapshots collections (schema + data + index definitions) to an S3-compatible bucket.
Install milvus-backup
wget https://github.com/zilliztech/milvus-backup/releases/latest/download/milvus-backup_Linux_x86_64.tar.gz
tar -xzf milvus-backup_Linux_x86_64.tar.gz
sudo mv milvus-backup /usr/local/bin/
milvus-backup --versionConfigure milvus-backup
Create /opt/milvus/backups/configs/backup.yaml pointing at your local Milvus and the MinIO bucket the Compose stack already runs:
log:
level: info
milvus:
address: localhost
port: 19530
minio:
address: localhost
port: 9000
accessKeyID: minioadmin
secretAccessKey: minioadmin
useSSL: false
bucketName: milvus-backup
rootPath: backup
backup:
maxSegmentGroupSize: 2G
parallelism:
backupCollection: 4
copydata: 128
restoreCollection: 2Create the backup bucket in MinIO (one-time):
docker exec milvus-minio mc mb /data/milvus-backup || trueCreate a Backup
milvus-backup create --config /opt/milvus/backups/configs/backup.yaml \
--name daily_$(date +%Y%m%d) \
--collections documents,docs_hybridRestore from a Backup
milvus-backup restore --config /opt/milvus/backups/configs/backup.yaml \
--name daily_20260416 \
--suffix _restoredOff-Site Backups
To move backups off the VPS, point the MinIO bucket at real S3 (edit the Compose file to use AWS credentials), or add a nightly cron that rclone syncs the MinIO milvus-backup bucket to a second region. Combined with restic snapshots of /opt/milvus/volumes, you have two independent recovery paths.
Cron for Daily Backups
sudo tee /etc/cron.d/milvus-backup > /dev/null <<'EOF'
0 3 * root milvus-backup create --config /opt/milvus/backups/configs/backup.yaml --name nightly_$(date +\%Y\%m\%d) >> /var/log/milvus-backup.log 2>&1
EOFKeep a retention window of 14-30 nightlies plus weekly and monthly snapshots. Delete old backups with milvus-backup delete --name <backup_name>.
When to Move to Milvus Cluster Mode
Milvus Standalone handles most workloads up to the single-node limits of your VPS: typically 50-200 million vectors at 768 dimensions with HNSW, assuming 32-64 GB RAM. You should plan for Milvus Cluster when any of these are true:
- Working set exceeds a single node's RAM, even with IVF_PQ or DISKANN
- Ingest throughput exceeds ~50k QPS sustained write
- You need multi-replica query nodes for high availability (Standalone has no replication)
- You need separate compute pools for ingest vs query to isolate noisy neighbors
- You are running across multiple availability zones
The upgrade path is smooth: backup your Standalone collections with milvus-backup, restore them into the new Cluster deployment, point your application at the new gRPC endpoint. Keep Standalone running in parallel during the migration if you want zero downtime.
Performance Tuning
Segment Size
Milvus writes data into segments. Smaller segments mean faster queries (less data scanned per segment) but more segments to merge. Tune via dataCoord.segment.maxSize in milvus.yaml (default 512 MB). For write-heavy workloads raise to 1024; for query-heavy keep lower.
ef at Query Time (HNSW)
The single most impactful knob for HNSW:
search_params={"metric_type": "COSINE", "params": {"ef": 128}}ef=32: fast, ~95% recallef=64: default, ~98% recallef=128: slower, ~99% recallef=256: slowest, ~99.5% recall
ef >= limit.nprobe (IVF family)
search_params={"params": {"nprobe": 32}}Higher nprobe = better recall, linear cost. Start at nprobe=16 and tune.
Load Only the Fields You Need
When calling load_collection, pass load_fields=["embedding", "text"] to keep less-used scalar columns off-heap.
Resource Limits in Docker Compose
Cap the Milvus container to prevent OOM on shared VPS:
services:
standalone:
deploy:
resources:
limits:
memory: 8g
reservations:
memory: 4gMonitor with Prometheus and Grafana
Milvus exposes Prometheus metrics at http://localhost:9091/metrics. Scrape with Prometheus, visualize in Grafana. Zilliz publishes an official Grafana dashboard (ID 18521).
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
milvus-standalone stuck restarting | etcd or MinIO not ready | Check docker compose logs etcd minio. Increase health-check timeout in compose file. Ensure /opt/milvus/volumes is writable. |
RPC error: connection refused from pymilvus | Wrong host/port or service still starting | Wait 60s after docker compose up. Verify curl http://localhost:9091/healthz returns OK. Check firewall: sudo ufw status. |
ParamError: metric type not match | Schema metric vs index metric mismatch | Drop the index and rebuild with the metric you used at insert time (COSINE vs L2 vs IP). |
No memory to load collection | Working set larger than configured limits | Reduce limit in search, remove unused fields from load_fields, switch HNSW to IVF_SQ8 or IVF_PQ, or upsize the VPS. |
Queries very slow at low ef | Index not loaded or wrong nprobe | Call client.load_collection() after any new index. Tune ef up to 128. Run client.get_load_state() to confirm. |
| Attu cannot connect from browser | Using localhost inside Docker | In the Attu connect dialog use milvus-standalone:19530 (the Docker service name), not localhost. |
| Disk usage growing indefinitely | Old segments not compacted | Call client.compact(collection_name) or set dataCoord.compaction.enableAutoCompaction: true. |
BM25 function not found in hybrid search | Milvus version < 2.5 | Upgrade: pull the latest milvusdb/milvus:v2.5.x image and docker compose up -d. |
Viewing Logs
sudo docker compose logs -f standalone
sudo docker compose logs -f etcd
sudo docker compose logs -f minioMilvus is verbose at INFO level. To quiet it, set log.level: warn in milvus.yaml.
FAQ
How does Milvus compare to Qdrant and Weaviate?
All three are excellent open-source vector databases with significant overlap. Milvus is the most mature and battle-tested at billion-scale, has the richest index menu (HNSW, IVF, DISKANN, GPU), and the strongest ecosystem for enterprise deployments. Qdrant has a simpler operational story, excellent filtered-search performance, and Rust-native implementation. Weaviate bundles modules for OpenAI, Cohere, and other model providers, making it a "batteries-included" RAG database.
For the single-VPS self-hosted scenario, pick Milvus when you expect to scale past 10 million vectors or need GPU indexes. Pick Qdrant for operational simplicity at small-to-medium scale. Pick Weaviate if you want built-in vectorization modules so you never write embedding code yourself. All three have excellent Python clients and integrate with LangChain and LlamaIndex.
Does Milvus require MinIO, or can I use S3?
Milvus requires any S3-compatible object store. MinIO is the default because it runs in the same Compose file, but you can point Milvus at AWS S3, Cloudflare R2, Backblaze B2, Contabo Object Storage, or Wasabi by editing minio.address, minio.accessKeyID, minio.secretAccessKey, and minio.bucketName in milvus.yaml. Using external object storage is the recommended pattern for production - it decouples storage lifecycle from compute lifecycle and simplifies cluster migrations.
Can Milvus handle real-time updates?
Yes. Inserts and deletes are reflected in query results within seconds (default consistency_level="Bounded"). For strict read-after-write guarantees pass consistency_level="Strong" at query time, which forces the query node to wait for the latest WAL entry. Upserts work via the upsert API, which deletes the old entity and inserts the new one in a single call. High update volume can fragment segments; run client.compact() nightly or enable enableAutoCompaction to merge small segments.
How do I secure Milvus for production?
Milvus supports RBAC with users, roles, and privileges. Enable authentication in milvus.yaml by setting common.security.authorizationEnabled: true and common.security.tlsMode: 2 for mTLS. Then create a root user and grant a least-privilege service account to your application. Place Milvus behind a private network, expose only 19530 to your application subnet, and use Caddy or Nginx if you need to expose gRPC externally. Never expose MinIO 9000 to the public internet - it is the backing object store.
What embedding model should I pair with Milvus?
For English text at small scale, all-MiniLM-L6-v2 (384-dim, ~80 MB) is fast and surprisingly good. For better quality use BAAI/bge-large-en-v1.5 (1024-dim) or intfloat/e5-large-v2 (1024-dim). For multilingual work use BAAI/bge-m3 (1024-dim) or sentence-transformers/paraphrase-multilingual-mpnet-base-v2 (768-dim). For code use jinaai/jina-embeddings-v2-base-code (768-dim). Run the embedder on the same VPS via Ollama with nomic-embed-text (768-dim) for a fully local stack where no data leaves your infrastructure.
How do I integrate Milvus with LangChain?
pip install langchain langchain-milvusfrom langchain_milvus import Milvus from langchain_ollama import OllamaEmbeddingsembeddings = OllamaEmbeddings(model="nomic-embed-text") vectorstore = Milvus( embedding_function=embeddings, connection_args={"uri": "http://localhost:19530"}, collection_name="langchain_docs", )
vectorstore.add_texts(["Milvus is a vector database.", "Ubuntu is a Linux distribution."]) docs = vectorstore.similarity_search("what is milvus", k=3)
The same pattern works with LlamaIndex (llama-index-vector-stores-milvus), Haystack (haystack-integrations-document-stores-milvus), and Dify (native Milvus connector in the Model Provider UI).
Next Steps
You now have a production-ready Milvus deployment with HNSW and BM25 hybrid indexing, partitions, Attu browser UI, and automated backups. Here is where to take it next:
- Build a full RAG pipeline - Combine this Milvus instance with Ollama for local LLM inference. Write a LangChain retriever that pulls the top-k chunks from your
documentscollection and feeds them into a Llama 3 prompt on the same VPS. End-to-end latency under 2 seconds, no data ever leaves your server. - Compare against Qdrant and Weaviate - Each vector database has different trade-offs. Our Qdrant install guide walks through the Rust-native alternative, and our Weaviate install guide covers the modules-first approach. Running the same dataset through all three is the fastest way to pick the right tool for your workload.
- Add observability - Scrape the
/metricsendpoint into Prometheus, import the Zilliz Grafana dashboard (ID 18521), and alert on query latency, memory pressure, and segment compaction lag. - Enable authentication and mTLS - Before exposing Milvus to any untrusted network, turn on RBAC in
milvus.yaml, create service accounts per application, and front the gRPC endpoint with mutual TLS. - Graduate to Cluster - When a single node cannot hold your working set, deploy Milvus on Kubernetes via the Milvus Operator. Use
milvus-backupto migrate collections with minimal downtime. - Read the official docs - The Milvus documentation covers advanced topics like time-travel queries, iterator search for very large k, binary vector indexes for hash-based similarity, and the Python SDK reference.
Need More Horsepower for Your Vector Stack?>
Milvus scales linearly with RAM. If you are pushing past 10 million vectors or running hybrid BM25 + HNSW over long-context chunks, size up to our higher CloudCore tiers.>
- 12 GB RAM - comfortably serves 2-5M 768-dim vectors with HNSW
- 24 GB RAM - 10-15M vectors, room for the LLM on the same box
- 48 GB RAM - 30-50M vectors, production RAG workloads
- 64+ GB RAM - enterprise-scale Standalone before graduating to Cluster>
Choose Your Plan at vps-server.host - all plans include unmetered bandwidth and NVMe storage.