🚀 Executive Summary
TL;DR: Exposing Kubernetes services on a KIND cluster running on a bare-metal VPS often results in LoadBalancer services stuck in
🎯 Key Takeaways
- KIND clusters on bare-metal VPS environments inherently lack the external load balancer functionality expected by LoadBalancer services, leading to a
state. - MetalLB is the recommended production-like solution for bare-metal KIND clusters, enabling LoadBalancer services to acquire routable IP addresses from a user-defined pool via ARP announcements.
- For exposing a fixed, small number of ports, KIND’s `extraPortMappings` configuration can directly map host VPS ports to the KIND control-plane node, typically used with an Ingress controller.
Struggling to expose services on a KIND Kubernetes cluster running on a VPS? This guide breaks down why it’s a pain and provides three real-world solutions, from quick port-forwards to a production-ready MetalLB setup.
Exposing Services on KIND: MetalLB, Port-Forwards, and Sanity on a Bare-Metal VPS
I remember it like it was yesterday. It was 10 PM, and I was trying to get a proof-of-concept running on a new Contabo VPS for a demo the next morning. The app was deployed, the pods were green, everything looked perfect… inside the cluster. But trying to hit the service from my browser? Nothing. Just a spinning wheel of doom. The `LoadBalancer` service I created was stuck in `
So, What’s Actually Going On?
Before we dive into the fixes, let’s understand the “why.” When you run KIND (Kubernetes in Docker), you’re creating a nested reality. Your Contabo VPS has a public IP address. On that VPS, Docker creates its own private network. Then, KIND spins up Docker containers (your “nodes”) on that private network. Finally, Kubernetes itself creates *another* network layer inside those nodes for your pods and services.
When you create a service of type `LoadBalancer`, Kubernetes expects something external—a cloud provider’s load balancer—to see this request and assign a real, routable IP address. On a bare-metal VPS, there is no such “something.” Kubernetes makes the request, and nobody answers. That’s why your service’s external IP stays `
Our job is to bridge this gap. Here are three ways to do it, ranging from a quick hack to the proper, long-term solution.
Solution 1: The “It’s 2 AM and I Just Need It to Work” Fix
Sometimes, you just need to see if the thing works. You don’t need a permanent IP or a fancy setup. You just need access *right now*. For this, my old friend kubectl port-forward is your best bet.
This command directly connects a port on your local machine (in this case, the VPS itself) to a port on a pod, service, or deployment inside the cluster.
Let’s say you have a service named `my-nginx-service` on port 80 in the `default` namespace. To access it from your VPS on port 8080, you run this:
kubectl port-forward service/my-nginx-service 8080:80
Now, on your VPS, you can access http://localhost:8080, and it will hit your service. Simple.
Warning: This is a temporary, foreground process. As soon as you close that terminal session or press Ctrl+C, the connection is gone. It’s fantastic for quick debugging but is absolutely not a permanent solution for exposing an application.
Solution 2: The “Do It Right” Fix with MetalLB
This is the real solution and the one you should aim for. MetalLB is a load-balancer implementation for bare-metal Kubernetes clusters. It simulates the behavior of a cloud provider’s load balancer by taking a pool of IP addresses you control and assigning them to services of type `LoadBalancer`.
Step 1: Install MetalLB
First, get MetalLB installed on your cluster. The manifest command is the easiest way:
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml
After a minute, you’ll see the MetalLB pods running in the `metallb-system` namespace.
Step 2: Configure MetalLB
This is the crucial part. You need to tell MetalLB which IP addresses it’s allowed to use. Let’s say your Contabo VPS has a public IP of `203.0.113.10` and a private IP of `192.168.1.100` on its main interface. You can tell MetalLB to use a small range from that same subnet. You create a file, let’s call it `metallb-config.yaml`:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: vps-ip-pool
namespace: metallb-system
spec:
addresses:
- 203.0.113.10-203.0.113.15 # Or a range on your VPS's private network
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: main-advertisement
namespace: metallb-system
Pro Tip: The IP range you provide in `IPAddressPool` must be on the same Layer 2 network as your VPS host. For a simple setup, just using the single public IP of your VPS (`203.0.113.10-203.0.113.10`) is often the easiest way to start. MetalLB will then use ARP to announce that it “owns” that IP for any service that requests it.
Apply the configuration:
kubectl apply -f metallb-config.yaml
Now, any `LoadBalancer` service you create will almost instantly get an IP from that pool. The `
Solution 3: The “KIND-Native” Fix
There’s another way that’s specific to KIND and simpler than MetalLB if you only need to expose a few, static ports. You can configure the KIND cluster itself to map ports from the host VPS directly to the KIND “nodes” (which are Docker containers).
This is done when you create the cluster. You need a config file, let’s call it `kind-cluster-config.yaml`:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
Then you create your cluster with this config:
kind create cluster --config kind-cluster-config.yaml
With this setup, any traffic hitting port 80 or 443 on your VPS (`dev-vps-01`) will be forwarded to ports 80 and 443 on the KIND control-plane node. You would then typically deploy an Ingress controller (like Nginx) using a `NodePort` service that listens on those ports. This method effectively “punches a hole” through the Docker network layer for specific ports.
Which One Should You Choose?
Here’s my simple breakdown:
| Method | Best For | Complexity |
| kubectl port-forward | Quick, temporary debugging. | Very Low |
| KIND extraPortMappings | Exposing a small, fixed number of ports (e.g., for an Ingress controller). | Low |
| MetalLB | The most flexible, “production-like” way to handle dynamic service exposure. | Medium |
For any serious development or staging environment on a VPS, my vote is always for MetalLB. It most closely mimics a real cloud environment and teaches the right habits. But don’t be ashamed to use the other tools when they fit the job. The best engineers I know aren’t the ones who know the most complex solution, but the ones who know which solution is the right size for the problem at hand.
🤖 Frequently Asked Questions
âť“ Why do my Kubernetes LoadBalancer services stay in on a KIND cluster on a VPS?
On a bare-metal VPS, KIND clusters lack an integrated cloud provider to assign external IP addresses to `LoadBalancer` services, causing them to remain in a `
âť“ How do MetalLB, `kubectl port-forward`, and KIND `extraPortMappings` compare for exposing services?
`kubectl port-forward` is a temporary solution for debugging. KIND `extraPortMappings` is suitable for exposing a small, fixed number of ports via cluster configuration. MetalLB provides a flexible, production-like solution for dynamic `LoadBalancer` service exposure by assigning IPs from a defined pool.
âť“ What is a common implementation pitfall when configuring MetalLB on a VPS?
A common pitfall is defining an `IPAddressPool` in MetalLB that is not on the same Layer 2 network as your VPS host. The IP range must be routable and on the same subnet as your VPS’s network interface for MetalLB to correctly announce ownership via ARP.
Leave a Reply