KubernetesNodePortKubernetes ServicesNetworkingDevOpsKubectl Beginner 15 min read

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?

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

TermMeaning
portThe port the Service listens on inside the cluster (ClusterIP port)
targetPortThe port on the Pod/container where the app is actually running
nodePortThe port opened on every Node’s IP for external access (30000–32767)

Why is NodePort Used?

NodePort is used when you need to:

  1. Expose an application externally without a cloud load balancer (e.g., in on-premises or bare-metal clusters).
  2. Local development and testing — especially with Minikube or kind, where cloud providers are unavailable.
  3. Quick demos and PoC — get traffic into a cluster in the simplest possible way.
  4. CI/CD pipeline testing — integration tests that need to call a live endpoint from outside the cluster.
  5. Air-gapped environments — where external load balancers or cloud integrations are not available.
  6. Learning Kubernetes networking — NodePort is the foundational concept before moving to LoadBalancer or Ingress.

When NOT to Use NodePort

  • Production workloads at scale — prefer LoadBalancer type or an Ingress controller.
  • 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

FeatureClusterIPNodePortLoadBalancerExternalName
Internal cluster access
External access
Cloud LB required
Use caseInternal microservicesDev/testing/bare-metalProduction on cloudDNS alias
Port rangeAny30000–32767AnyN/A
CostFreeFreeCloud LB costFree

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)
  • kubectl configured 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 80 is the ClusterIP port (internal).
  • Port 30080 is 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

  1. Always specify nodePort explicitly — auto-assigned ports are hard to document and firewall.

    nodePort: 30080    # Pin it for predictability
    
  2. Use 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 number
    
  3. Add 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: 10
    
  4. Set 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: Local
    
  5. Use NodePort for dev/test only — in production on cloud, use LoadBalancer or an Ingress controller.

  6. Document your NodePort assignments — maintain a port registry to avoid conflicts across services.

  7. Restrict NodePort range — tighten the range (--service-node-port-range) to a smaller set of known ports for better security posture.


Summary

ConceptKey Takeaway
WhatA Service type that opens a static port on every cluster node for external access
Port range30000–32767 (default), customizable
Traffic pathExternalClient → NodeIP:NodePort → ClusterIP:Port → Pod:TargetPort
Implemented bykube-proxy using iptables/IPVS rules on every node
Best forLocal dev (Minikube), bare-metal, on-prem, CI testing
Not ideal forProduction-scale cloud workloads (use LoadBalancer + Ingress)
Most common issueLabel selector mismatch → kubectl get endpoints shows <none>
Debug first stepkubectl 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.