NodePort: a port on every node
The most straightforward way to let traffic into the cluster from outside, and its cost.
When ClusterIP isn't enough
ClusterIP gives a Service an internal address inside the cluster. Pods and other services see it, but it's unreachable from outside the cluster. Admin tasks or a quick check of the app need external access. NodePort solves this in the most direct way: it opens a port on every node.
What NodePort is
When a Service is created with type: NodePort, Kubernetes does three
things:
- Creates a ClusterIP, like a regular Service: an internal address.
- Opens an extra port on every node in the cluster in the range
30000-32767. kube-proxy (the component on every node that sets up the network rules for Services) routes this port to the same Pods that sit behind the ClusterIP. - Any request to
<any-node>:<NodePort>lands on one of the Service's Pods.
Minimal manifest
apiVersion: v1
kind: Service
metadata:
name: nodeapp
spec:
type: NodePort
selector:
app: nodeapp
ports:
- port: 80 # internal ClusterIP port
targetPort: 8080 # container port
nodePort: 30090 # external port on every node
Fields
type: NodePort: required. Without it you get a regular ClusterIP.selector: which Pods receive traffic. Must match thelabelson the Pod template in the Deployment.port: the internal ClusterIP port. Other Services in the cluster talk to this.targetPort: the container port the Service forwards to. Must matchcontainerPorton the Pod.nodePort: the external port. If you don't set it, Kubernetes picks a free one from the range itself. An explicit number is needed when you need a predictable address.
After creation, kubectl get svc shows both ports separated by a colon:
first port, then nodePort.
kubectl get svc nodeapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
nodeapp NodePort 10.96.113.229 <none> 80:30090/TCP 31s
Port range
By default 30000-32767. It's a compromise: high enough to not conflict
with system ports, and wide enough for a single cluster to handle hundreds
of services.
If you try to set nodePort: 80, the API server responds:
The Service "nodeapp" is invalid: spec.ports[0].nodePort: Invalid value: 80: provided port is not in the valid range. The range of valid ports is 30000-32767
The range is changed with the --service-node-port-range flag on
kube-apiserver. That's a control plane setting for the whole cluster,
nobody touches it for a single Service.
How to check
First, check that the Service was created correctly and found the Pods:
kubectl get svc nodeapp
# PORT(S) column: 80:30090/TCP
kubectl get endpoints nodeapp
# a non-empty list of IPs = the Service really points at Pods
The v1 Endpoints object is deprecated since 1.33, and kubectl prints a
warning:
Warning: v1 Endpoints is deprecated in v1.33+; use discovery.k8s.io/v1 EndpointSlice
The same list of addresses through the current object:
kubectl get endpointslice -l kubernetes.io/service-name=nodeapp
You can hit the NodePort with a request from inside the cluster: a
temporary Pod sends a request to node-IP:30090.
NODE_IP=$(kubectl get nodes \
-o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
kubectl run np-check --rm -it --restart=Never \
--image=busybox:1.36.1 -- wget -qO- http://$NODE_IP:30090
# should get the nginx welcome page
NodePort in kind
In kind, a cluster node lives inside a Docker container. From inside the
cluster, a request to node-IP:<nodePort> gets through. But from a laptop
on macOS and Windows, the node IP isn't visible: Docker Desktop keeps
containers inside its own virtual machine. That's why the node port is
mapped to the laptop ahead of time in the cluster config, through
extraPortMappings:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30090
hostPort: 30090
listenAddress: "127.0.0.1"
Now http://127.0.0.1:30090 on the laptop leads to port 30090 on the node,
and from there to the NodePort Service. The mapping is only set when the
cluster is created. Details in the kind documentation,
Extra Port Mappings.
What matters here
One port for the whole cluster
If two different Services ask for nodePort: 30090, the second one won't
get created:
spec.ports[0].nodePort: Invalid value: 30090: provided port is already allocated
NodePort is unique within the cluster, even if the Services live in different namespaces.
Every node listens on this port
Even on the ones with no Pod of this Service. kube-proxy still routes the traffic to a Pod on another node. This simplifies external load balancers: any node address works.
externalTrafficPolicy: Cluster vs Local
The default is Cluster: a request to node A can land on a Pod on node B.
This balances the load, but loses the client IP: the Pod sees the node's
address.
Local: traffic only goes to a Pod on the same node the request arrived
at. The client IP is preserved. But if there's no Pod of this Service on
the node, kube-proxy doesn't forward the request anywhere, and the client
won't get a response.
| Value | Where the request lands | Client IP | If there's no Pod on the node |
|---|---|---|---|
Cluster (default) | any Pod of the Service, even on another node | lost | the request still gets through |
Local | only a Pod on this same node | preserved | the request doesn't reach a Pod |
spec:
type: NodePort
externalTrafficPolicy: Local
When to use NodePort
- Local development and a quick check of the app.
- A minimal cluster without a LoadBalancer provider.
- Behind an L4 load balancer (HAProxy, AWS NLB) that sends to
<any-node>:30090.
When not to use it
- In production with a large number of services. Fixed high ports are
awkward for DNS,
:30090doesn't immediately tell you what it is. - When you need HTTPS termination. NodePort works at L4, the certificate has to go on the Pod or on an Ingress.
- When you need rules by host or path. That's Ingress or Gateway API, covered further in the track.
NodePort opens a port on all nodes in the cluster, including the ones with no Pod of this Service. Without a firewall in front of the nodes, this port is visible from the internet. In production, nodes are hidden behind a load balancer or restricted with network rules.
The quick path without YAML
kubectl create deployment nodeapp --image=nginxinc/nginx-unprivileged:1.25
kubectl expose deployment nodeapp --port=80 --target-port=8080 --type=NodePort
# which port the cluster handed out
kubectl get svc nodeapp -o jsonpath='{.spec.ports[0].nodePort}'
kubectl expose without an explicit nodePort takes a random port from
the range. For a predictable address, add the port to the manifest by
hand.
Common mistakes
- Forgetting
type: NodePortand only settingnodePort. The API won't accept such a manifest, the error will sayspec.ports[0].nodePort: Forbidden: may not be used whenfollowed by something about theClusterIPtype. - Mixing up
portandnodePort. Internal clients go toportthrough the Service name, external ones knock onnodePortat a node's address. targetPortdoesn't match the container's port. The Service gets created, the type is right, there are addresses in the EndpointSlice, but a request from outside gets no response.- In kind, knocking from the laptop on a port that isn't mapped through
extraPortMappings. Everything works inside the cluster, nothing works outside.
What you'll need for the task
- Change an existing Service's type to NodePort so internal clients don't
notice a difference: the name and
portstay the same. - Set
nodePortexplicitly: only one node port is mapped to the laptop. - Check the result with
kubectl get svc(thePORT(S)column) and acurlrequest from the laptop to127.0.0.1.
In short
type: NodePortopens the same port from the range30000-32767on every node in the cluster.- ClusterIP always stays on top of NodePort, NodePort just adds an external entry point.
- The port is unique within the cluster: two Services can't share one
nodePort. externalTrafficPolicy: Localpreserves the client IP, but a request to a node with no Pod of this Service doesn't get through.- For production, NodePort only works behind a load balancer, HTTPS and host-based rules are handled by Ingress or Gateway API.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 1 lab for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson k8s-network-02-service-nodeport
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress