End-to-End Guide: Deploying Envoy Gateway on Bare-Metal Kubernetes
This guide walks through deploying Envoy Gateway on a self-managed, bare-metal Kubernetes cluster (like RKE2 on VMware) and routing external traffic to an application using nip.io.
Because this is a bare-metal environment without a cloud provider, we must manually patch the Envoy Gateway service to mimic a cloud LoadBalancer provisioning an IP.
Step 1: Install the Gateway API CRDs
Envoy Gateway requires the modern Kubernetes Gateway API definitions to function. These must be installed into the cluster first.
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.1.0/standard-install.yaml
Step 2: Install Envoy Gateway
We use Helm to deploy the Envoy Gateway controller. We include --skip-crds because we manually installed them in the previous step.
# Create the namespace
kubectl create namespace envoy-gateway-system
# Install Envoy Gateway via Helm
helm install otterscale-envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
--version v1.7.4 -n envoy-gateway-system --skip-crds
Wait a few moments and verify the controller is running:
kubectl get pods,svc -n envoy-gateway-system
Step 3: Create the GatewayClass
The GatewayClass links any Gateway you create to the specific Envoy Gateway controller we just installed. Without this, your Gateways will stay in a <pending> state.
Save as gatewayclass.yaml and apply:
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: envoy
spec:
controllerName: gateway.envoyproxy.io/gatewayclass-controller
kubectl apply -f gatewayclass.yaml
Step 4: Deploy the Target Application (Podinfo)
Deploy a sample application and expose it internally via a ClusterIP Service.
Save as podinfo.yaml and apply:
apiVersion: apps/v1
kind: Deployment
metadata:
name: podinfo
spec:
replicas: 1
selector:
matchLabels:
app: podinfo
template:
metadata:
labels:
app: podinfo
spec:
containers:
- name: podinfo
image: stefanprodan/podinfo
ports:
- containerPort: 9898
---
apiVersion: v1
kind: Service
metadata:
name: podinfo
spec:
selector:
app: podinfo
ports:
- port: 9898
targetPort: 9898
kubectl apply -f podinfo.yaml
Step 5: Configure the Gateway and HTTPRoute
Now we define the physical entry point (Gateway) and the routing rules (HTTPRoute).
- The Gateway listens on port 80.
- The HTTPRoute looks for the hostname
podinfo.<YOUR_IP>.nip.ioand sends traffic to the internalpodinfoservice.
(Replace 10.0.50.10 with the actual IP address of your bare-metal server).
Save as routing.yaml and apply:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: standard-web-gateway
namespace: envoy-gateway-system
spec:
gatewayClassName: envoy
listeners:
- name: web-listener
port: 80
protocol: HTTP
allowedRoutes:
namespaces:
from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: podinfo-route
namespace: default
spec:
parentRefs:
- name: standard-web-gateway
namespace: envoy-gateway-system
hostnames:
- "podinfo.10.0.50.10.nip.io"
rules:
- backendRefs:
- name: podinfo
port: 9898
kubectl apply -f routing.yaml
Step 6: The Bare-Metal Patch
At this point, the Envoy controller has created a LoadBalancer service for your Gateway, but it is stuck in <pending> because there is no cloud provider to assign it an IP.
We must manually patch the service with the bare-metal server's IP address to unblock the controller.
Run these commands, substituting your actual IP address:
# 1. Store the auto-generated service name in a variable
SVC=$(kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=standard-web-gateway -o name)
# 2. Patch the service with your server IP (Replace 10.0.50.10)
kubectl patch $SVC \
-n envoy-gateway-system \
--subresource=status \
--type=merge \
-p '{"status":{"loadBalancer":{"ingress":[{"ip":"10.0.50.10"}]}}}'
Step 7: Verify and Test
Check that the Gateway is now showing as Programmed: True and has an address assigned:
kubectl get gateway standard-web-gateway -n envoy-gateway-system
Finally, test the connection using the wildcard DNS:
curl http://podinfo.10.0.50.10.nip.io/
You should successfully see the Podinfo response!