Kubernetes Service: NodePort

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Expose a Service on a static port across every node. How NodePort layers on ClusterIP, the 30000-32767 range, externalTrafficPolicy, and when not to use it.

A NodePort Service opens the same port on every node in the cluster and forwards anything arriving there to the Service's Pods, including nodes running none of those Pods.

It is the simplest way to reach a Service from outside a cluster, and it is rarely the right way in production.

It builds on ClusterIP

NodePort does not replace ClusterIP; it adds to it. Creating a NodePort Service gives you all three of:

  1. A ClusterIP, working exactly as before for internal callers.
  2. A node port in the 30000 to 32767 range, open on every node.
  3. Forwarding from that port to the Service's endpoints.
apiVersion: v1
kind: Service
metadata:
  name: payments-api
spec:
  type: NodePort
  selector:
    app: payments-api
  ports:
    - port: 80           # the ClusterIP port
      targetPort: 8080   # the Pod's port
      nodePort: 30080    # the port on every node (optional)
kubectl get service payments-api
NAME           TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
payments-api   NodePort   10.96.41.203    <none>        80:30080/TCP   4s

80:30080/TCP reads as "ClusterIP port 80, node port 30080".

Omit nodePort and Kubernetes assigns a free one. Prefer that unless you need a fixed value. Hardcoding invites collisions, since a node port is cluster-wide and two Services cannot share one.

The three ports, again

This is where people lose track, because there are now three numbers:

FieldReached byExample
nodePortExternal clients, via any node's IP30080
portIn-cluster clients, via the ClusterIP or DNS80
targetPortThe Service, forwarding to the Pod8080

So curl http://<node-ip>:30080 from outside and curl http://payments-api:80 from inside reach the same Pods on port 8080.

# Find a node IP to test against
kubectl get nodes -o wide
curl http://192.168.1.24:30080/healthz

On minikube there is a shortcut:

minikube service payments-api --url

Why the port range is what it is

30000 to 32767 avoids colliding with well-known ports and with anything the node's own OS is likely to use. You cannot expose a NodePort on 80 or 443 without changing an API server flag, which is not something to do casually.

That restriction is one of the main reasons NodePort is a poor production front door. Nobody wants to publish http://api.example.com:31847.

externalTrafficPolicy: the setting that matters

By default (Cluster), traffic arriving at any node is forwarded to a Pod on any node. That gives even distribution and means every node is a valid entry point, but the extra hop source-NATs the packet, so your application sees the node's IP rather than the client's.

spec:
  externalTrafficPolicy: Local

With Local, a node only forwards to Pods running on itself. The client IP is preserved and the extra hop disappears, at the cost of two things: nodes with no matching Pod drop the traffic, and load becomes uneven if Pods are unevenly distributed.

Cluster (default)Local
Client source IPLost (SNAT)Preserved
Extra network hopPossibleNever
Nodes without a PodForward anywayDrop the traffic
Load distributionEvenFollows Pod placement

If you need real client IPs, for rate limiting, geolocation, allowlisting, or audit logs, use Local and make sure something in front only sends traffic to nodes that have Pods. Cloud load balancers do this automatically via health checks, which is exactly why Local is common on LoadBalancer Services.

Why it is a poor production choice

Every node opens the port, including nodes running none of the Service's Pods. On a 50-node cluster that is 50 open ports for one application.

You still need something in front. Nodes come and go, so clients cannot target a node IP directly. You need a load balancer, which is what a LoadBalancer Service creates for you automatically.

The port range is unusable for a public URL.

No TLS, no host or path routing. That is what an Ingress provides.

Where NodePort genuinely fits:

  • Local development on minikube or kind.
  • Behind an external load balancer you manage yourself, particularly on bare metal.
  • Non-HTTP protocols where an Ingress does not apply and you have your own load balancer.

For anything public and HTTP, use an Ingress. For non-HTTP traffic on a cloud provider, use LoadBalancer.

Debugging

Work outward from the Pods.

# 1. Does the Service have endpoints at all?
kubectl get endpointslices -l kubernetes.io/service-name=payments-api

# 2. Does it work internally?
kubectl run tmp --rm -it --image=nicolaka/netshoot -- curl -s payments-api/healthz

# 3. What is the assigned node port?
kubectl get svc payments-api -o jsonpath='{.spec.ports[0].nodePort}'

# 4. Can you reach the node at all?
kubectl get nodes -o wide

If it works internally but not from outside, the Service is fine and the problem is network access to the node. On a cloud provider, the node's security group or firewall rule almost certainly does not allow inbound traffic on the 30000 to 32767 range, that is the single most common cause. Check too whether you are using a private node IP from outside the VPC.

Quick reference

TaskCommand
Create from a Deploymentkubectl expose deployment <n> --type=NodePort --port=80 --target-port=8080
Find the node portkubectl get svc <n> -o jsonpath='{.spec.ports[0].nodePort}'
List node IPskubectl get nodes -o wide
URL on minikubeminikube service <n> --url
Check endpointskubectl get endpointslices -l kubernetes.io/service-name=<n>
Detailkubectl describe svc <n>

Key takeaways

  • NodePort includes a ClusterIP. It adds external access rather than replacing internal access.
  • The port opens on every node, whether or not that node runs a matching Pod.
  • The range is 30000 to 32767, which rules it out as a public URL.
  • Three ports now: nodePort outside, port inside, targetPort on the Pod.
  • externalTrafficPolicy: Cluster hides the client IP. Use Local to preserve it, accepting uneven load.
  • Works internally but not externally? Check the node firewall or security group on the NodePort range.
  • Use Ingress for public HTTP and LoadBalancer for non-HTTP on a cloud provider.

Check your understanding

Exercise

Every request from the same IP

An application behind a NodePort Service logs the same handful of source IPs for all traffic, matching the cluster nodes. Rate limiting by client IP is therefore useless. Why?

Exercise

Unreachable from outside

A NodePort Service works when curled from inside the cluster but times out from a laptop using a node's public IP and the node port. What should you check first?


Frequently Asked Questions

What is a NodePort Service and how does it work?

It exposes a Service on a static port, between 30000 and 32767, on every node in the cluster. Traffic arriving at any node's IP on that port is forwarded to the Service's Pods, even if that particular node runs none of them. A NodePort Service also includes a ClusterIP, so internal callers are unaffected. NodePort adds an external path rather than replacing the internal one.

Why is the NodePort range limited to 30000-32767?

The range is chosen to avoid colliding with well-known service ports and with ports the node's own operating system may use. Exposing a Service on 80 or 443 directly would require changing the --service-node-port-range flag on the API server, which is not something to do casually. This restriction is a large part of why NodePort is unsuitable as a public entry point. Nobody wants to publish a URL with port 31847 in it.

What is the difference between NodePort and LoadBalancer?

LoadBalancer builds on NodePort. It does everything NodePort does and additionally asks the cloud provider to create a real external load balancer that distributes traffic across the nodes and gives you a stable public address. NodePort leaves you to solve that yourself, which is fine on bare metal where you already run a load balancer and awkward on a cloud provider where LoadBalancer does it in one line.

Should I use NodePort in production?

Usually not. Every node opens the port regardless of whether it runs the workload, the port range is unusable for a public URL, and you still need something in front to spread traffic across nodes and handle nodes coming and going. For public HTTP traffic use an Ingress, which gives you hostname and path routing plus TLS through a single load balancer. NodePort is genuinely useful for local development with minikube or kind, and on bare metal behind a load balancer you manage yourself.

Why does my application see the node's IP instead of the client's?

Because externalTrafficPolicy defaults to Cluster, which allows a node to forward traffic to a Pod on another node, and that second hop requires source NAT, replacing the client address. Setting externalTrafficPolicy: Local keeps traffic on the receiving node and preserves the original source IP. The trade-off is that nodes with no matching Pod drop traffic rather than forwarding it, so whatever sits in front must health-check and only send traffic to nodes that have Pods.

My NodePort Service works inside the cluster but not from outside. What is wrong?

If it works internally, the Service, its selector and its endpoints are all correct, so the problem is network reachability to the node. On a cloud provider the node's security group or firewall almost certainly does not allow inbound traffic on 30000 to 32767, which is the most frequent cause. Also confirm you are using an IP that is actually reachable from where you are testing. A private node IP will not work from outside the VPC.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources