Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

NodePort Service Expose

Learn how to expose a Kubernetes Deployment using a NodePort service with a hands-on walkthrough, comparison of options, troubleshooting, and next steps.

Focus: expose a deployment via nodeport service

Sponsored

You've deployed your first Kubernetes workload — great! But right now, that deployment is an island: only Pods inside the cluster can talk to it, and there's no way for users, APIs, or external load balancers to reach your application. This is the Kubernetes service gap: deployments manage Pods, but they don't create network endpoints. Without a service, your app is invisible to the outside world. That's exactly the problem NodePort solves — it opens a dedicated port on every cluster Node and routes traffic straight to your Pods.

The problem this lesson solves

Kubernetes deployments handle scaling, self-healing, and rolling updates, but they don't expose a stable network entry point. Pods come and go — their IPs change, and they can't be reached from outside the cluster by default. If you want to test a web app locally, demo a service to a teammate, or run an internal tool without a cloud load balancer, you need a simple, cluster-wide way to expose your deployment. NodePort fills that gap: it maps a high-numbered port (30000–32767) on every Node to a service that forwards traffic to your Pods.

Core concept / mental model

Think of NodePort as a club bouncer standing at every entrance of a venue (your cluster Nodes). The bouncer knows the exact room (your Pod) where the party is happening, no matter which entrance a guest uses. In Kubernetes terms:

  • NodePort creates a Service of type NodePort.
  • The Service allocates a static port (e.g., 30080) across all Nodes in the cluster.
  • A kube-proxy component on each Node watches for Services and installs iptables rules to forward traffic arriving at <NodeIP>:<NodePort> to healthy Pods.
  • The Service acts as the stable front door: Pods die or scale, but the NodePort + Service selector keeps traffic flowing.

Blockquote pro tip: NodePort is often a stepping stone to LoadBalancer (on cloud providers) or Ingress (for HTTP routing). It's the simplest way to go from "inside cluster only" to "reachable from your laptop."

How it works step by step

  1. Label your Deployment Pods — make sure they carry a key=value label (e.g., app: web-backend) that the Service will select.
  2. Create a Service manifest of type NodePort — define a selector that matches the label from step 1.
  3. Choose a port — you can either let Kubernetes auto-assign a NodePort (random in 30000–32767) or specify one explicitly.
  4. kube-proxy takes over — on every Node, it watches for new NodePort Services and programs iptables rules to DNAT (Destination NAT) inbound packets to the ClusterIP and then to a Pod.
  5. Access your app — using <any-Node-IP>:<NodePort>. For example, if your minikube VM is at 192.168.49.2 and the NodePort is 30080, hit http://192.168.49.2:30080.

Critical detail: targetPort vs. port vs. nodePort

  • spec.ports[].port — the port that the Service itself listens on (ClusterIP internal).
  • spec.ports[].targetPort — the port on the Pod container (must match your Pod's containerPort).
  • spec.ports[].nodePort — (optional) the port opened on each Node. If omitted, Kubernetes picks one.
apiVersion: v1
kind: Service
metadata:
  name: my-nodeport-svc
spec:
  type: NodePort
  selector:
    app: web-backend
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080

Hands-on walkthrough

Let's expose a real Deployment. Assume we have an nginx deployment named web-deploy with label app: nginx. Follow along in your terminal (minikube or kind cluster).

1. Create the Deployment

kubectl create deployment web-deploy --image=nginx:latest --replicas=2
kubectl get pods --show-labels

Expected output:

NAME                          READY   STATUS    RESTARTS   AGE   LABELS
web-deploy-7d4b4f9b8f-abcde   1/1     Running   0          30s   app=web-deploy
web-deploy-7d4b4f9b8f-xyz12   1/1     Running   0          30s   app=web-deploy

2. Create the NodePort Service

kubectl expose deployment web-deploy --type=NodePort --port=80 --target-port=80
kubectl get svc web-deploy

Expected output:

NAME         TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
web-deploy   NodePort   10.96.192.100   <none>        80:32567/TCP   10s

Notice: 32567 is the auto-assigned NodePort. The Service's ClusterIP is 10.96.192.100.

3. Access the application

If you're on minikube, retrieve the Node IP:

minikube ip
# Typically 192.168.49.2

Then curl:

curl http://192.168.49.2:32567

You should see the default Nginx welcome HTML page.

4. Inspect iptables (for deeper understanding)

# On any Node (minikube ssh into the VM)
minikube ssh
sudo iptables -t nat -L KUBE-SERVICES
# Look for lines with your NodePort

Compare options / when to choose what

Service Type External Reachability Use Case Cloud Provider? Notes
ClusterIP ❌ Inside cluster only Internal microservices No Default type
NodePort ✅ Static port on every Node Dev/test, on-prem, demos No Simple, but don't use in prod unless you have good network controls
LoadBalancer ✅ Cloud LB with external IP Production HTTP/TCP services Yes Automatic cloud LB provisioning (costly)
Ingress (with Controller) ✅ Smart HTTP/S routing Multiple services under one IP Usually yes Requires Ingress controller (e.g., nginx-ingress)

When to choose NodePort: - You need quick external access for testing. - You're running Kubernetes on bare metal or air-gapped environments where LoadBalancer isn't an option. - You want a simple load-balanced entry point without a cloud bill.

Troubleshooting & edge cases

Problem: "Connection refused" or timeout

# Check service exists and NodePort is allocated
kubectl get svc
# Check Pods are running and matching labels
kubectl get pods --show-labels
# Verify your Node's firewall allows the NodePort range
# On Linux: sudo ufw status | grep 30000
# On minikube: ensure no other service uses same NodePort
kubectl get svc --all-namespaces | grep 300

Problem: NodePort port collision

If you specify a nodePort that's already in use, the Service creation fails with:

Error from server: Service "my-svc" is invalid: spec.ports[0].nodePort: Invalid value: 30080: provided port is already allocated

Fix: Use kubectl get svc --all-namespaces -o=custom-columns=PORT:.spec.ports[*].nodePort to check used ports, or let Kubernetes auto-assign.

Edge case: Traffic routing inside the Node

  • kube-proxy do not automatically source-NAT traffic from the same Node to itself. If a request comes to NodeIP:NodePort but the target Pod is on the same Node, the reply comes from the Pod's IP, not the Node's. This can confuse clients expecting the source IP. Kubernetes handles this via externalTrafficPolicy: Local — but that's an advanced topic.

Edge case: NodePort with externalTrafficPolicy: Cluster (default)

  • Traffic can be sent to any Node, then forwarded to a Pod on a different Node (extra hop). Good for load balancing, bad for client IP preservation.

What you learned & what's next

You now know how to expose a deployment via NodePort service — from writing the manifest to curling your app on a Node IP. You understand the three-port hierarchy (port, targetPort, nodePort), compared NodePort with other service types, and debugged common pitfalls like port collisions and connectivity drops. This skill is essential for any developer moving from "cluster internal" to "accessible outside."

Next up: Learn Service DNS discovery — how to reach services by name (my-svc.namespace.svc.cluster.local) inside the cluster, and how kube-dns or CoreDNS makes it seamless.

Ready to go deeper? Try the practice recap below.

Practice recap

Create a new Deployment with kubectl create deployment whoami --image=containous/whoami --replicas=3. Then expose it as a NodePort Service on port 8888 targeting container port 80. Use curl $(minikube ip):<auto-assigned-nodeport> to verify. After that, delete the service, define a YAML service with nodePort: 30300, and re-deploy. Confirm you can hit the app on port 30300. This reinforces both imperative and declarative approaches to NodePort exposure.

Common mistakes

  • Forgetting to match the Service selector with the Pod labels. If labels don't align, the Service won't forward traffic (check kubectl describe svcEndpoints field).
  • Specifying a nodePort that falls outside the allowed range (30000–32767) — the API server rejects it with an explicit error.
  • Assuming NodePort opens only on one Node — it actually opens on every Node, which can surprise firewall admins.
  • Using port and targetPort interchangeably: port is the Service's internal port, targetPort is the Pod's container port. Swapping them can cause Connection refused.

Variations

  1. Use kubectl expose for quick ad-hoc exposure without writing a YAML file.
  2. Write a declarative YAML manifest for GitOps workflows — always preferred for production.
  3. Use nodePort: 0 (or omit) to let Kubernetes auto-assign; this is great for CI/CD pipelines where port conflicts must be avoided.

Real-world use cases

  • Exposing a staging web app on a bare-metal Kubernetes cluster to internal QA testers via a dedicated high port (e.g., :30080).
  • Running a Redis admin UI on-premises for ops teams: use NodePort on a known port (e.g., 31000) and restrict access via corporate VPN.
  • Setting up a temporary performance test endpoint where AWS/GCP LoadBalancer would incur cost — NodePort on a cluster node is free.

Key takeaways

  • NodePort opens the same port (30000–32767) on every Node, routing traffic to selected Pods via the Service's selector.
  • The three-port model — port (Service), targetPort (container), nodePort (Node) — must align to work correctly.
  • kube-proxy implements iptables (or IPVS) rules to DNAT inbound NodePort traffic to Pod IPs.
  • NodePort is the simplest external exposure method; prefer it for dev/test, but switch to LoadBalancer or Ingress for production HTTP/S.
  • Always verify Service Endpoints are populated (kubectl get endpoints) — empty endpoints mean label mismatch or no ready Pods.
  • Use externalTrafficPolicy: Local if you need client source IP preservation and don't mind uneven load distribution.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.