I was tinkering with a RAG project at home and needed a vector database. So I deployed Milvus onto my single-node K8s cluster at home.

Environment Info
My lab environment is a physical machine with Rocky Linux 10.1 installed, running a single-node Kubernetes cluster:
- OS: Rocky Linux 10.1
- Container runtime: cri-o
- CNI: Flannel
- Helm: v3.x
Pre-Checks: Storage and Network
Before deploying Milvus, two things must be confirmed first: storage and network.
Milvus depends on etcd and minio, both of which need PVCs. First check whether the cluster has a default StorageClass:
|
If the output is empty, there’s no storage class configured. For a single-machine test environment I used hostPath to set up a few local PVs, which is enough for lab scenarios:
|
Note: the
hostPathcapacity is only a declaration; the actual usage depends on how much data is written, but you must make sure the host machine’s disk space is sufficient.
Adding the Helm Repo and Installing
Milvus officially provides a Helm Chart; first add the repo:
|
I chose the Standalone mode, with all components running in a single Pod and Woodpecker as the message queue. For a single-machine test, this combination saves resources while being sufficient:
|
After installing, check the Pod status:
|
Normally you should see all three Pods in Running state:
| Pod | Expected status |
|---|---|
milvus-etcd-0 |
1/1 Running |
milvus-minio-xxx |
1/1 Running |
milvus-standalone-xxx |
1/1 Running |
But my situation wasn’t so smooth—the following three pitfalls popped up one after another.
Pitfall One: Image Pull Failure (docker.io Unreachable)
Symptom: The Pod status is stuck in ImagePullBackOff, and in the Events you can see records of connection to docker.io timing out.
Cause: My network environment can’t reach the official docker.io registry.
Solution: Configure a registry mirror for cri-o. I used DaoCloud’s image accelerator:
|
After configuring and restarting cri-o, delete the Pods that failed to pull and let the kubelet re-pull the images.
Pitfall Two: Pod-to-Pod Network Unreachable (firewalld Blocking CNI Traffic)
This was the most troublesome pitfall.
Symptom: etcd and minio both start normally, but milvus-standalone stays in CrashLoopBackOff. The logs repeatedly show this kind of error:
|
Inside the Pod, not only can it not reach etcd, but DNS resolution also fails and the ClusterIP can’t be pinged either.
Cause: firewalld‘s nftables rules were blocking traffic on Flannel’s cni0 bridge. Combined with the kernel parameter bridge-nf-call-iptables=1, bridge traffic enters nftables and is then dropped, cutting off all Pod-to-Pod communication.
Solution: Run the following on the host machine with root privileges:
|
After restarting kube-proxy, Pod-to-Pod communication, DNS resolution, and ClusterIP access all returned to normal. milvus-standalone could finally reach etcd too.
It’s advised to run
systemctl disable firewalldin advance, to avoid firewalld starting again after a node reboot and interfering with the network.
Pitfall Three: etcd Permission Error
Symptom: The etcd Pod won’t start, and the logs report:
|
Cause: bitnami’s etcd image runs as a non-root user by default, but the directory mounted by hostPath doesn’t have enough permission.
Solution: Crude but effective:
|
Then delete the etcd Pod and let it be recreated.
Exposing the Service
The Service deployed by default is of type ClusterIP, accessible only within the cluster. I chose the NodePort approach, which suits single-machine testing:
|
The access addresses after exposure:
| Purpose | Address |
|---|---|
| Milvus gRPC/REST API | http://<node IP>:30001 |
| Milvus WebUI | http://<node IP>:30002 |
Connection Test
After deployment, verify with the Python client:
|
Or use the REST API to view the list of databases and collections:
|
Note: directly visiting
http://<ip>:19530/in a browser returns404 page not found, because this port serves the gRPC/REST API, not a web root page.
Uninstall and Cleanup
If you no longer need it, you can uninstall with one command:
|
To thoroughly clean up the PVs and local data:
|
Deployment Parameter Notes
Finally, a summary of the key Helm parameters used this time:
| Helm parameter | Value | Description |
|---|---|---|
cluster.enabled |
false |
Standalone mode, all components running in a single Pod |
image.all.tag |
v2.6.14 |
Milvus image version |
pulsarv3.enabled |
false |
Disable Pulsar |
woodpecker.enabled |
true |
Enable Woodpecker as the message queue |
streaming.enabled |
true |
Enable the Streaming node |
etcd.replicaCount |
1 |
Single etcd replica |
minio.mode |
standalone |
MinIO standalone mode |
minio.replicas |
1 |
Single MinIO replica |
Summary
This Milvus deployment experience once again confirmed a truth: with a local test environment, never underestimate the two basic problems of network and storage.
Of the three pitfalls, firewalld blocking CNI traffic was the most hidden and the most time-consuming. On the surface it looked like Milvus couldn’t connect to etcd, but in fact the Pod-to-Pod network of the entire cluster was broken. To troubleshoot this kind of problem, the most direct method is to manually curl or telnet another service’s ClusterIP from inside the Pod, to quickly locate whether it’s a DNS problem, a routing problem, or a firewall problem.
If you’re also deploying Milvus on a single-node K8s, I hope this article helps you step into a few fewer pitfalls.
References
评论
0 条评论