Kubernetes: NGINX NodePort Deployment

A NodePort Service is the most direct way to reach an application running inside Kubernetes from outside the cluster. Kubernetes reserves one port on every node and forwards anything arriving on it to the pods behind the Service, so the application answers on <any node IP>:<nodePort> — including nodes that are not currently running a replica.

This guide deploys NGINX, scales it to three replicas, exposes it with a NodePort Service, and tests it from both outside and inside the cluster. It assumes you already have a cluster running; if you do not, start with installing a Kubernetes cluster on Ubuntu.

How NodePort relates to the other Service types

The three main Service types are layered, each adding to the one before it:

  • ClusterIP — the default. A virtual IP that only resolves inside the cluster.
  • NodePort — everything ClusterIP does, plus one port opened on every node.
  • LoadBalancer — everything NodePort does, plus an external load balancer with an address of its own. That is covered separately in exposing NGINX with a LoadBalancer Service.

Creating a NodePort Service therefore also creates a ClusterIP behind it. A request arriving from outside travels from the node IP and node port, to the Service's ClusterIP, to one of the pods — with kube-proxy on the receiving node picking the pod and forwarding the packet, even if the chosen pod lives on a different node.

How a request reaches a pod through a NodePort Service A client outside the cluster sends a request to port 31234 on any one of the three node IP addresses. Every node listens on that same node port. The node forwards the request to the Service nginx-np, which holds the cluster IP 10.96.24.7 on port 8888. The Service then forwards the request to one of the three NGINX pods, each listening on container port 80. External client curl http://10.0.0.12:31234 any node IP works Kubernetes cluster node-1 10.0.0.11:31234 node-2 10.0.0.12:31234 node-3 10.0.0.13:31234 Service nginx-np ClusterIP 10.96.24.7:8888 nginx pod port 80 nginx pod port 80 nginx pod port 80
Three ports are in play: the node port (31234) that the outside world connects to, the Service port (8888) on the ClusterIP, and the container port (80) that NGINX actually listens on.

Step 1: Optional — allow deployments on control plane nodes

Control plane nodes carry a taint that stops ordinary workloads being scheduled on them. On a single-node or small lab cluster you may want to remove it so your three replicas have somewhere to run:

kubectl taint nodes --all node-role.kubernetes.io/control-plane-

The trailing hyphen is what removes the taint. On a multi-node cluster with dedicated workers, skip this step.

Step 2: Create the Deployment

Deploy the NGINX application:

kubectl create deployment nginx --image=nginx

Step 3: Scale the Deployment

Scale to three replicas so there is something for the Service to load balance across:

kubectl scale deployment/nginx --replicas=3

Confirm all three are running before continuing:

kubectl get pods -o wide

Step 4: Expose the Deployment with a NodePort

kubectl expose deployment/nginx --type=NodePort --port=8888 --target-port=80 --name=nginx-np

This is the step people most often misread, because --port is not the port you connect to from outside:

  • --port=8888 is the port on the Service's ClusterIP, used by other pods inside the cluster.
  • --target-port=80 is the container port NGINX listens on.
  • The node port is not specified here at all. Kubernetes allocates a free one automatically from the range 30000–32767 (set cluster-wide by the API server's --service-node-port-range flag).

Step 5: Find the allocated node port

kubectl get service nginx-np

The PORT(S) column shows both numbers:

NAME       TYPE       CLUSTER-IP     EXTERNAL-IP   PORT(S)          AGE
nginx-np   NodePort   10.96.24.7     <none>        8888:31234/TCP   30s

Read that as Service port 8888, node port 31234. The number after the colon is the one to use from outside. To script it rather than read it off:

kubectl get svc nginx-np -o jsonpath='{.spec.ports[0].nodePort}'

Step 6: Test it from outside the cluster

List the node addresses, then connect to any one of them on the node port:

kubectl get nodes -o wide
curl http://<node ip>:31234

You should get the NGINX welcome page back. Try a different node IP with the same port — it works from every node, not just the ones hosting a pod.

Step 7: Test it from inside the cluster

Inside the cluster the ClusterIP and Service port apply instead. Get a pod name, open a shell in it, and curl the Service:

kubectl get pod
kubectl exec -it <pod name> -- /bin/bash

Then, from inside the pod:

curl http://nginx-np:8888
curl http://10.96.24.7:8888

Both work: cluster DNS resolves the Service name to its ClusterIP, which is why in-cluster clients should use the name rather than a hard-coded address.

Step 8: Pinning a specific node port (optional)

An automatically allocated port is fine for testing, but it changes if the Service is recreated. To fix it, set nodePort explicitly in a manifest:

apiVersion: v1
kind: Service
metadata:
  name: nginx-np
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
    - port: 8888
      targetPort: 80
      nodePort: 30080

The value must sit inside the cluster's node port range, and must not already be taken by another Service.

Common mistakes

  • Connecting to the Service port from outside. http://<node ip>:8888 will not answer — 8888 only exists on the ClusterIP, inside the cluster.
  • A firewall in the way. Cloud security groups and host firewalls such as ufw rarely allow 30000–32767 by default. If the curl times out rather than being refused, suspect the firewall first.
  • Assuming the node must host a pod. Every node listens on the node port and forwards as needed.
  • Leaving the Service in place. A NodePort is open on every node in the cluster, so remove it when you have finished testing.

When to use NodePort — and when not to

NodePort is ideal for labs, bare-metal clusters with no cloud integration, and quick verification that a Deployment actually serves traffic. It is a poor fit for production web traffic: the port numbers are unfriendly, there is no TLS termination or host and path routing, and clients need to know node addresses that change as the cluster changes. For real traffic, put something in front of it — either a LoadBalancer Service, or an external NGINX load balancer pointing at the cluster.

Step 9: Cleanup

Remove the Deployment and Service when you are done:

kubectl delete deployment nginx
kubectl delete service nginx-np