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
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
- Label your Deployment Pods — make sure they carry a
key=valuelabel (e.g.,app: web-backend) that the Service will select. - Create a Service manifest of type
NodePort— define a selector that matches the label from step 1. - Choose a port — you can either let Kubernetes auto-assign a NodePort (random in 30000–32767) or specify one explicitly.
- 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.
- Access your app — using
<any-Node-IP>:<NodePort>. For example, if your minikube VM is at192.168.49.2and the NodePort is30080, hithttp://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:
32567is the auto-assigned NodePort. The Service's ClusterIP is10.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:NodePortbut 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 viaexternalTrafficPolicy: 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
selectorwith the Pod labels. If labels don't align, the Service won't forward traffic (checkkubectl describe svc→Endpointsfield). - Specifying a
nodePortthat falls outside the allowed range (30000–32767) — the API server rejects it with an explicit error. - Assuming
NodePortopens only on one Node — it actually opens on every Node, which can surprise firewall admins. - Using
portandtargetPortinterchangeably:portis the Service's internal port,targetPortis the Pod's container port. Swapping them can causeConnection refused.
Variations
- Use
kubectl exposefor quick ad-hoc exposure without writing a YAML file. - Write a declarative YAML manifest for GitOps workflows — always preferred for production.
- 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
Endpointsare populated (kubectl get endpoints) — empty endpoints mean label mismatch or no ready Pods. - Use
externalTrafficPolicy: Localif you need client source IP preservation and don't mind uneven load distribution.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.