NodePort Service
Learn what a NodePort Service is in Kubernetes, how it works, and how to expose applications externally using worker node IP addresses and ports.
NodePort Service in Kubernetes
Table of Contents
- What is NodePort?
- Why is NodePort Used?
- How NodePort Works
- NodePort vs Other Service Types
- Where NodePort is Used
- NodePort Architecture Diagram
- Hands-On: Creating a NodePort Service
- NodePort Port Range and Customization
- NodePort with Multiple Replicas
- Real-World Troubleshooting Scenarios
- NodePort Troubleshooting Cheat Sheet
- Best Practices
- Summary
What is NodePort?
NodePort is one of the four types of Kubernetes Services. It is the simplest way to expose a Kubernetes application to external traffic — i.e., traffic originating from outside the Kubernetes cluster.
When you create a NodePort Service, Kubernetes allocates a static port (between 30000–32767 by default) on every Node in the cluster. Any traffic that arrives at <NodeIP>:<NodePort> is automatically forwarded to the correct Pod(s) running inside the cluster, regardless of which node the Pod lives on.
External Traffic --> NodeIP:NodePort --> ClusterIP:Port --> Pod:TargetPort
A Kubernetes Service is an abstraction that defines a logical set of Pods and a policy to access them. Without a Service, Pods are only reachable within the cluster using their ephemeral IP addresses — which change every time a Pod is restarted. NodePort solves the external access problem without requiring a cloud load balancer.
Key Terminology
| Term | Meaning |
|---|---|
port | The port the Service listens on inside the cluster (ClusterIP port) |
targetPort | The port on the Pod/container where the app is actually running |
nodePort | The port opened on every Node’s IP for external access (30000–32767) |
Why is NodePort Used?
NodePort is used when you need to:
- Expose an application externally without a cloud load balancer (e.g., in on-premises or bare-metal clusters).
- Local development and testing — especially with Minikube or kind, where cloud providers are unavailable.
- Quick demos and PoC — get traffic into a cluster in the simplest possible way.
- CI/CD pipeline testing — integration tests that need to call a live endpoint from outside the cluster.
- Air-gapped environments — where external load balancers or cloud integrations are not available.
- Learning Kubernetes networking — NodePort is the foundational concept before moving to LoadBalancer or Ingress.
When NOT to Use NodePort
- Production workloads at scale — prefer
LoadBalancertype or anIngresscontroller. - When you need SSL termination — use an Ingress controller with TLS.
- When you need path-based routing — NodePort exposes one service per port; Ingress handles multiple services on one IP.
- When port management becomes complex — maintaining unique port numbers across many services is error-prone.
How NodePort Works
NodePort builds on top of the ClusterIP service type. Here is the full packet journey:
Step-by-Step Traffic Flow
1. External client sends: HTTP GET http://192.168.49.2:30080/
2. The request hits the Node's network interface on port 30080.
3. kube-proxy (running as a DaemonSet on every Node) intercepts the packet
using iptables or IPVS rules.
4. kube-proxy translates the destination:
NodeIP:30080 --> ClusterIP:80
5. The ClusterIP is a virtual IP inside the cluster. kube-proxy further
load-balances across all healthy Pod endpoints:
ClusterIP:80 --> PodIP:8080 (round-robin)
6. The Pod receives the request on container port 8080 and responds.
7. The response travels back through the same path.
The Role of kube-proxy
kube-proxy is the key component that makes NodePort work. It runs on every node and watches the Kubernetes API for Service and Endpoint changes. When a NodePort Service is created:
- kube-proxy writes iptables rules (or IPVS rules) that intercept packets arriving on the assigned NodePort.
- These rules perform DNAT (Destination Network Address Translation) — rewriting the destination IP from the Node’s IP to a Pod IP.
- Load balancing across multiple Pod replicas happens at this layer using round-robin by default.
Port Mapping Visual
┌──────────────────────────────────────────────────────┐
│ Kubernetes Node │
│ │
│ External Traffic │
│ 192.168.49.2:30080 ──► kube-proxy (iptables) │
│ │ │
│ ▼ │
│ ClusterIP Service :80 │
│ │ │ │
│ ┌────┘ └────┐ │
│ ▼ ▼ │
│ Pod-1 :8080 Pod-2 :8080 │
└──────────────────────────────────────────────────────┘
NodePort vs Other Service Types
| Feature | ClusterIP | NodePort | LoadBalancer | ExternalName |
|---|---|---|---|---|
| Internal cluster access | ✅ | ✅ | ✅ | ✅ |
| External access | ❌ | ✅ | ✅ | ✅ |
| Cloud LB required | ❌ | ❌ | ✅ | ❌ |
| Use case | Internal microservices | Dev/testing/bare-metal | Production on cloud | DNS alias |
| Port range | Any | 30000–32767 | Any | N/A |
| Cost | Free | Free | Cloud LB cost | Free |
Where NodePort is Used
1. Local Development with Minikube
The most common real-world use case. Minikube provides a single-node cluster with a local IP. NodePort is the standard way to access services:
minikube service my-service --url
# Output: http://127.0.0.1:30080
2. On-Premises / Bare Metal Kubernetes
Companies running Kubernetes on bare-metal servers (without cloud providers) use NodePort together with an external load balancer (like HAProxy or F5) sitting in front of the nodes.
Internet --> HAProxy (80/443) --> Node1:30080 or Node2:30080 --> Pods
3. kubeadm Clusters in Labs
In training environments or home labs created with kubeadm, NodePort is how students and engineers test externally accessible workloads without a cloud account.
4. CI/CD Integration Tests
A CI pipeline running on a VM external to the cluster can call a NodePort to run smoke tests against a freshly deployed service before promoting to production.
5. IoT and Edge Kubernetes (K3s)
Lightweight distributions like K3s on edge devices often use NodePort for device-to-device or device-to-cloud communication at fixed ports.
NodePort Architecture Diagram
┌─────────────┐
│ External │
│ Client │
└──────┬──────┘
│
http://NODE_IP:30080
│
┌────────────────────┼────────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Node 1 │ │ Node 2 │ │ Node 3 │
│ :30080 ✅ │ │ :30080 ✅ │ │ :30080 ✅ │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────────┼────────────────────┘
│
┌──────────▼──────────┐
│ ClusterIP Service │
│ 10.96.100.5:80 │
└──────────┬──────────┘
│ (load balances)
┌───────────────┼───────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Pod-1 │ │ Pod-2 │ │ Pod-3 │
│ 10.244.1.5 │ │ 10.244.2.3 │ │ 10.244.3.7 │
│ :8080 │ │ :8080 │ │ :8080 │
└─────────────┘ └─────────────┘ └─────────────┘
Note: Even if you connect to Node 2 or Node 3, the request can be routed to a Pod running on Node 1. kube-proxy handles cross-node routing transparently.
Hands-On: Creating a NodePort Service
Prerequisites
- Minikube installed and running (
minikube start) kubectlconfigured to point to your Minikube cluster
# Verify cluster is running
kubectl cluster-info
kubectl get nodes
Step 1: Deploy a Sample Application
We’ll deploy an NGINX web server as our sample application.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.25
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
kubectl apply -f deployment.yaml
# Verify pods are running
kubectl get pods -l app=nginx
# NAME READY STATUS RESTARTS AGE
# nginx-deployment-7c79c4bf97-4xkbm 1/1 Running 0 30s
# nginx-deployment-7c79c4bf97-9fzpw 1/1 Running 0 30s
# nginx-deployment-7c79c4bf97-xm7qk 1/1 Running 0 30s
Step 2: Create a NodePort Service via YAML
This is the recommended approach for production and reproducibility.
# nodeport-service.yaml
apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport
labels:
app: nginx
spec:
type: NodePort
selector:
app: nginx # Must match the Pod labels from your Deployment
ports:
- name: http
protocol: TCP
port: 80 # ClusterIP port (internal)
targetPort: 80 # Container port where NGINX listens
nodePort: 30080 # External port on every Node (optional: omit to auto-assign)
kubectl apply -f nodeport-service.yaml
# Verify service is created
kubectl get service nginx-nodeport
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# nginx-nodeport NodePort 10.96.175.233 <none> 80:30080/TCP 10s
Understanding the PORT(S) column: 80:30080/TCP means:
- Port
80is the ClusterIP port (internal). - Port
30080is the NodePort (external).
Step 3: Create a NodePort Service via kubectl expose
A quick imperative alternative — useful for quick tests:
kubectl expose deployment nginx-deployment \
--type=NodePort \
--port=80 \
--target-port=80 \
--name=nginx-nodeport-quick
# Check the auto-assigned NodePort
kubectl get service nginx-nodeport-quick
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# nginx-nodeport-quick NodePort 10.96.88.10 <none> 80:31456/TCP 5s
When you omit
--node-port, Kubernetes auto-assigns a port from the 30000–32767 range.
Step 4: Verify and Access the Service
On Minikube
# Get the Minikube node IP
minikube ip
# 192.168.49.2
# Access using minikube service command (opens browser automatically)
minikube service nginx-nodeport --url
# http://192.168.49.2:30080
# Test with curl
curl http://$(minikube ip):30080
# <!DOCTYPE html>
# <html>
# <head><title>Welcome to nginx!</title>...
On a Multi-Node Cluster (kubeadm/bare-metal)
# Get any node's external IP
kubectl get nodes -o wide
# NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP
# node1 Ready control-plane 5d v1.29.0 192.168.1.10 <none>
# node2 Ready <none> 5d v1.29.0 192.168.1.11 <none>
# Access via any node IP
curl http://192.168.1.10:30080
curl http://192.168.1.11:30080 # Both work!
Describe the Service for Full Details
kubectl describe service nginx-nodeport
# Name: nginx-nodeport
# Namespace: default
# Labels: app=nginx
# Selector: app=nginx
# Type: NodePort
# IP Family Policy: SingleStack
# IP Families: IPv4
# IP: 10.96.175.233
# IPs: 10.96.175.233
# Port: http 80/TCP
# TargetPort: 80/TCP
# NodePort: http 30080/TCP
# Endpoints: 10.244.0.5:80,10.244.1.3:80,10.244.2.7:80
# Session Affinity: None
# External Traffic Policy: Cluster
# Events: <none>
The Endpoints field shows the actual Pod IPs receiving traffic. If this is empty, your selector labels are mismatched.
NodePort Port Range and Customization
Default Range: 30000–32767
The default NodePort range is set in the kube-apiserver configuration. You can view it:
# On a kubeadm cluster
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep node-port-range
# --service-node-port-range=30000-32767
Changing the NodePort Range (kubeadm)
# In /etc/kubernetes/manifests/kube-apiserver.yaml
spec:
containers:
- command:
- kube-apiserver
- --service-node-port-range=20000-40000 # Custom range
⚠️ Changing this range requires restarting the API server and may affect existing services.
Reserving Specific Ports
To always get the same NodePort (for firewall rules or documentation), explicitly set nodePort in your Service YAML:
ports:
- port: 80
targetPort: 80
nodePort: 30080 # Fixed port — fails if already in use
NodePort with Multiple Replicas
NodePort automatically load-balances across all healthy Pod replicas. Let’s see this in action:
# Scale to 5 replicas
kubectl scale deployment nginx-deployment --replicas=5
# Watch endpoints update
kubectl get endpoints nginx-nodeport
# NAME ENDPOINTS AGE
# nginx-nodeport 10.244.0.5:80,10.244.1.3:80,10.244.2.7:80 + 2 more... 5m
Every request to NodeIP:30080 is load-balanced across all 5 pods in round-robin fashion by kube-proxy’s iptables rules.
Session Affinity (Sticky Sessions)
By default, there is no session affinity. Each request may go to a different Pod. To enable sticky sessions (route a client to the same Pod):
spec:
type: NodePort
sessionAffinity: ClientIP # Stick based on client IP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 3600 # Keep sticky for 1 hour
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30080
Real-World Troubleshooting Scenarios
Scenario 1: Connection Refused on NodePort
Symptom:
curl http://192.168.49.2:30080
# curl: (7) Failed to connect to 192.168.49.2 port 30080: Connection refused
Diagnosis Steps:
# Step 1: Check if the service exists and has the right type
kubectl get service nginx-nodeport
# If not found: service doesn't exist
# Step 2: Check if NodePort is in the valid range
kubectl describe service nginx-nodeport | grep NodePort
# NodePort: http 30080/TCP <-- looks fine
# Step 3: Check if pods are running
kubectl get pods -l app=nginx
# Are they in Running state? Any CrashLoopBackOff?
# Step 4: Check endpoints — are Pods being selected?
kubectl get endpoints nginx-nodeport
# NAME ENDPOINTS AGE
# nginx-nodeport <none> 2m <-- PROBLEM: no endpoints!
# Step 5: Check pod labels vs service selector
kubectl get pods --show-labels | grep nginx
# nginx-deployment-xxx app=web <-- label is 'web', not 'nginx'!
kubectl describe service nginx-nodeport | grep Selector
# Selector: app=nginx <-- service expects 'nginx'
Root Cause: Label mismatch between Service selector and Pod labels.
Fix:
# Option A: Update the service selector
kubectl patch service nginx-nodeport -p '{"spec":{"selector":{"app":"web"}}}'
# Option B: Update the deployment labels
kubectl label pods -l app=web app=nginx --overwrite
Scenario 2: NodePort Works Locally but Not Externally
Symptom:
# Works from inside the node:
curl http://localhost:30080 # ✅ 200 OK
# Fails from external machine:
curl http://WORKER_NODE_PUBLIC_IP:30080 # ❌ timeout
Diagnosis Steps:
# Step 1: Check firewall rules on the node
# Ubuntu/Debian
sudo ufw status
sudo iptables -L INPUT -n | grep 30080
# CentOS/RHEL
sudo firewall-cmd --list-ports | grep 30080
# Step 2: Check Security Groups (AWS EC2 / cloud VMs)
# In AWS Console: EC2 --> Security Groups --> Inbound Rules
# Must allow TCP port 30080 from 0.0.0.0/0 (or your IP)
# Step 3: Check if the NodePort is actually listening
ss -tlnp | grep 30080
# If empty, kube-proxy may not have created the iptables rule
# Step 4: Check kube-proxy status
kubectl get pods -n kube-system | grep kube-proxy
kubectl logs -n kube-system kube-proxy-xxxxx | tail -20
Root Cause: Cloud firewall (Security Group / NSG) or OS firewall blocking the port.
Fix:
# Ubuntu: Open the port
sudo ufw allow 30080/tcp
sudo ufw reload
# AWS: Add inbound rule via AWS CLI
aws ec2 authorize-security-group-ingress \
--group-id sg-xxxxxxxx \
--protocol tcp \
--port 30080 \
--cidr 0.0.0.0/0
Scenario 3: Pod Not Receiving Traffic
Symptom: Service responds but the application returns errors (500, 502, 503).
Diagnosis Steps:
# Step 1: Verify endpoints are healthy
kubectl get endpoints nginx-nodeport
# NAME ENDPOINTS AGE
# nginx-nodeport 10.244.0.5:80,10.244.1.3:80 5m
# Step 2: Test direct pod connectivity (bypass the service)
kubectl exec -it debug-pod -- curl http://10.244.0.5:80
# Can you reach the pod directly?
# Step 3: Check pod logs for errors
kubectl logs nginx-deployment-7c79c4bf97-4xkbm --tail=50
kubectl logs nginx-deployment-7c79c4bf97-4xkbm --previous # last crashed container
# Step 4: Check pod readiness
kubectl get pods -l app=nginx -o wide
# Is READY column showing 1/1?
# Step 5: Describe pod for events
kubectl describe pod nginx-deployment-7c79c4bf97-4xkbm
# Look for: Readiness probe failed, OOMKilled, ImagePullBackOff
# Step 6: Check if targetPort matches containerPort
kubectl describe service nginx-nodeport | grep TargetPort
# TargetPort: 8080/TCP <-- service sends to 8080
kubectl describe pod nginx-deployment-7c79c4bf97-4xkbm | grep -A5 Ports
# Ports: 80/TCP <-- but container listens on 80!
# MISMATCH!
Root Cause: targetPort in Service doesn’t match the container’s actual listening port.
Fix:
kubectl patch service nginx-nodeport \
--type='json' \
-p='[{"op":"replace","path":"/spec/ports/0/targetPort","value":80}]'
Scenario 4: NodePort Service Returns 502 Bad Gateway
Symptom:
curl http://192.168.49.2:30080
# <html><body><h1>502 Bad Gateway</h1></body></html>
Diagnosis Steps:
# Step 1: Check pod status — are they all Running?
kubectl get pods -l app=nginx
# NAME READY STATUS RESTARTS AGE
# nginx-deployment-xxx-4xkbm 0/1 CrashLoopBackOff 5 3m
# Step 2: Check pod logs
kubectl logs nginx-deployment-xxx-4xkbm
# Error: failed to bind to 0.0.0.0:80: permission denied
# Step 3: Check resource limits — is the pod OOMKilled?
kubectl describe pod nginx-deployment-xxx-4xkbm | grep -A3 "Last State"
# Last State: Terminated
# Reason: OOMKilled
# Step 4: Check if the container is actually starting
kubectl get events --field-selector involvedObject.name=nginx-deployment-xxx-4xkbm
Root Cause: Pods are crashing on startup due to permission errors or OOM.
Fix (OOMKilled):
# Increase memory limit in deployment
resources:
limits:
memory: "512Mi" # Increase from 128Mi
cpu: "500m"
Fix (Permission denied on port 80):
# Use a non-privileged port in the container, map via targetPort
containers:
- name: nginx
ports:
- containerPort: 8080 # App listens here
---
# In Service:
ports:
- port: 80
targetPort: 8080 # Route to 8080
nodePort: 30080
NodePort Troubleshooting Cheat Sheet
# ── SERVICE INSPECTION ─────────────────────────────────────────────────────
kubectl get svc # List all services
kubectl get svc nginx-nodeport -o yaml # Full service definition
kubectl describe svc nginx-nodeport # Human-readable details
# ── ENDPOINT HEALTH ────────────────────────────────────────────────────────
kubectl get endpoints nginx-nodeport # Check if pods are selected
# If ENDPOINTS shows <none>: label selector mismatch or pods not Ready
# ── POD STATUS ─────────────────────────────────────────────────────────────
kubectl get pods -l app=nginx -o wide # Pods + node placement
kubectl get pods -l app=nginx --show-labels # Verify labels
# ── CONNECTIVITY TESTS ─────────────────────────────────────────────────────
# Test ClusterIP (inside cluster)
kubectl run test-pod --rm -it --image=curlimages/curl -- curl http://CLUSTER_IP:80
# Test NodePort (from outside)
curl http://NODE_IP:30080
# Test Pod directly (bypass service)
kubectl exec -it test-pod -- curl http://POD_IP:8080
# ── LOGS ───────────────────────────────────────────────────────────────────
kubectl logs -l app=nginx --tail=100 # All pod logs
kubectl logs -l app=nginx -f # Follow logs
kubectl logs POD_NAME --previous # Crashed container logs
# ── KUBE-PROXY ─────────────────────────────────────────────────────────────
kubectl get pods -n kube-system -l k8s-app=kube-proxy
kubectl logs -n kube-system -l k8s-app=kube-proxy --tail=50
# Check iptables rules for NodePort (on a node)
sudo iptables -t nat -L KUBE-NODEPORTS -n | grep 30080
# ── EVENTS ─────────────────────────────────────────────────────────────────
kubectl get events --sort-by='.lastTimestamp' # Recent cluster events
kubectl get events -n default --field-selector reason=Failed
# ── QUICK FIXES ────────────────────────────────────────────────────────────
# Restart all pods in deployment
kubectl rollout restart deployment nginx-deployment
# Force delete stuck pod
kubectl delete pod POD_NAME --grace-period=0 --force
# Delete and recreate service
kubectl delete svc nginx-nodeport
kubectl apply -f nodeport-service.yaml
Best Practices
Always specify
nodePortexplicitly — auto-assigned ports are hard to document and firewall.nodePort: 30080 # Pin it for predictabilityUse named
targetPort— reference the container port by name, not number. This makes your Service resilient to port number changes.# In Deployment: ports: - name: http containerPort: 80 # In Service: targetPort: http # References the name, not the numberAdd readiness probes — kube-proxy only routes traffic to Pods that pass readiness checks. Always define them.
readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 5 periodSeconds: 10Set
externalTrafficPolicy: Local— prevents cross-node traffic hops, preserves the client’s real IP, and reduces latency. Trade-off: only nodes with the Pod running will respond.spec: type: NodePort externalTrafficPolicy: LocalUse NodePort for dev/test only — in production on cloud, use
LoadBalanceror an Ingress controller.Document your NodePort assignments — maintain a port registry to avoid conflicts across services.
Restrict NodePort range — tighten the range (
--service-node-port-range) to a smaller set of known ports for better security posture.
Summary
| Concept | Key Takeaway |
|---|---|
| What | A Service type that opens a static port on every cluster node for external access |
| Port range | 30000–32767 (default), customizable |
| Traffic path | ExternalClient → NodeIP:NodePort → ClusterIP:Port → Pod:TargetPort |
| Implemented by | kube-proxy using iptables/IPVS rules on every node |
| Best for | Local dev (Minikube), bare-metal, on-prem, CI testing |
| Not ideal for | Production-scale cloud workloads (use LoadBalancer + Ingress) |
| Most common issue | Label selector mismatch → kubectl get endpoints shows <none> |
| Debug first step | kubectl describe svc <name> + kubectl get endpoints <name> |
NodePort is the gateway concept to understanding all Kubernetes networking. Once you master NodePort, LoadBalancer services and Ingress controllers become natural extensions of the same fundamental idea: routing external traffic to the right Pods.
Next: 03 - LoadBalancer Service — Exposing applications on cloud providers using managed load balancers.