Skip to main content
DevOps Insightsguides & answers

Containers · intermediate

Understanding Kubernetes Networking: Services, Ingress, and DNS

Short answer

Every pod gets its own IP and can reach every other pod directly — there is no port mapping and no NAT between pods. A Service is a stable name and address in front of a changing set of pods, chosen by labels. When something cannot be reached, the cause is almost always that the Service selects nothing, or that the pods it selects are not Ready.

N
Neeraj Jha
Updated 8 min read220 views
On this page · 13 sections
Diagram: traffic from a client through an Ingress to a Service and on to three Pods, with the Service's cluster DNS name.

Who this is for

You can deploy a pod and you cannot reliably get traffic to it. You do not need to understand CNI plugins or kube-proxy internals to fix the common failures.

The model

Kubernetes requires that every pod can reach every other pod by IP, across nodes, without NAT. That is why there is no -p 8080:80 here: the pod already has an address, and the only question is how anyone finds it.

Pod IPs are not stable — a replacement pod gets a new one (a container restarting inside the same pod keeps it). A Service is the stable thing in front of them.

Service types

yaml
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web          # <- matches pod LABELS, not pod names
  ports:
    - port: 80        # the port the Service listens on
      targetPort: 8080  # the port the container listens on
TypeReachable fromUse
ClusterIPInside the cluster onlyThe default, and what most things want
NodePortAny node IP on a high portDevelopment, or behind your own load balancer
LoadBalancerThe internet, via a cloud LBOne cloud load balancer per Service — this gets expensive
ExternalName—A CNAME to something outside the cluster

port and targetPort are different things and mixing them up produces a Service that resolves and then refuses the connection, because traffic arrives at a port nothing in the pod is listening on.

For StatefulSets you often want clusterIP: None — a headless Service, which returns the pod IPs directly instead of load-balancing, so each replica is individually addressable.

How a Service finds its pods

The selector matches labels, every matching pod is listed as an endpoint, and only the ones marked Ready receive traffic:

bash
kubectl get endpointslices -l kubernetes.io/service-name=web
kubectl get endpoints web          # deprecated since v1.33, but lists only Ready pods

If the EndpointSlice lists no addresses, the selector matches nothing. If it lists addresses but kubectl describe endpointslice shows Ready: false for every one of them, the pods are not Ready — the plain listing shows not-Ready pods too. Those are the only two causes:

The labels do not match. A Service with app: web and pods labelled app: web-server select nothing. There is no error — Kubernetes has no opinion about labels you have not created yet.

The pods are not Ready. Readiness gates endpoint membership deliberately: a pod that is starting, or whose readiness probe is failing, is removed from the Service so traffic is not sent to it. A failing readiness probe therefore looks exactly like a networking problem from the outside. This site covers that one directly: kubectl says my service has no endpoints.

Ingress

An Ingress is a request for HTTP routing. (The Ingress API is frozen — still supported, no longer developed — and the Kubernetes project now recommends Gateway API for new work. The community ingress-nginx controller stopped receiving releases and security fixes after March 2026, so if that is what you run, plan a move to a maintained controller.) On its own it does nothing — a controller has to be running to act on it. Creating an Ingress in a cluster with no controller produces a resource that stays there, with no address, and no error explaining why.

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  ingressClassName: nginx     # which controller should pick this up
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
  tls:
    - hosts: ["app.example.com"]
      secretName: app-tls

ingressClassName matters as soon as there is more than one controller, and omitting it in a cluster with no default class is a quiet way to have nothing happen at all.

When the Ingress is working and the backend is not, you get a 502 from the controller rather than a Kubernetes error — see 502 Bad Gateway.

DNS

CoreDNS gives every Service a name:

web                          # same namespace
web.production               # across namespaces
web.production.svc.cluster.local   # fully qualified

A detail worth knowing: pods get ndots:5 in /etc/resolv.conf, which means any name with fewer than five dots is first tried against each cluster search domain. Looking up api.github.com can therefore cost several failed queries before the real one. It is usually invisible, and it shows up as latency under load. A trailing dot (api.github.com.) skips the search list.

When DNS itself stops working the symptoms are broad and confusing — DNS stops resolving walks through what actually happens in the cluster.

Deprecations to plan for

Added 27 October 2026. Three parts of this picture now carry end dates, checked against the Kubernetes release notes and source up to 1.37:

  • kube-dns was deprecated in 1.37, and no new packages are expected after v1.40. CoreDNS has been the default since 1.13. To see which you run: kubectl -n kube-system get deploy kube-dns coredns. A Service named kube-dns proves nothing, because kubeadm keeps that name for CoreDNS.
  • kube-proxy's ipvs mode was deprecated in 1.35. kube-proxy 1.37 says it will be disabled by default in 1.40 and removed in 1.43, in favour of nftables. Check with kubectl -n kube-system get configmap kube-proxy -o yaml | grep -n 'mode:'.
  • Service spec.externalIPs was deprecated in 1.36. Every apply of such a Service now prints a warning, and support goes completely no earlier than 1.43. Move those Services to type: LoadBalancer or a Gateway.

Each item, with its source and the check to run, is on the Kubernetes upgrade checklist.

Network policies

Default behaviour is allow-all. A pod becomes restricted only once some NetworkPolicy selects it — and then, for the directions that policy mentions, everything not explicitly allowed is denied.

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: web-allow-from-ingress
spec:
  podSelector:
    matchLabels:
      app: web
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - port: 8080

Two traps. Writing an Ingress-only policy does not restrict egress at all, which people assume it does. And policies need a CNI that implements them — on a cluster whose CNI does not, the resource is accepted and silently ignored, which is the worst of both outcomes.

Debugging, in order

bash
kubectl describe endpointslices -l kubernetes.io/service-name=web  # 1. any endpoints, and Ready: true?
kubectl get pods -l app=web -o wide                            # 2. are they Ready?
kubectl describe svc web                                       # 3. selector and ports
kubectl run tmp --rm -it --image=nicolaka/netshoot -- bash     # 4. test from inside
  # then:  curl -v web.production.svc.cluster.local
  #        nslookup web.production

Do step 1 first every time. It takes five seconds and it separates "nothing is behind this Service" from every other possible cause, which is most of the diagnosis.

Common mistakes

  1. Selector and labels not matching. Silent, and the most common cause of an empty endpoint list.
  2. port where targetPort was meant. Resolves, then the connection is refused.
  3. A LoadBalancer Service per application. That is a cloud load balancer each, billed hourly. An Ingress controller gives you one for all of them.
  4. Expecting an Ingress to work with no controller installed. Nothing errors; nothing happens.
  5. Assuming a readiness failure is a network failure. It removes the pod from the Service, which looks identical from outside.

If this did not work

  • Endpoints exist, connection still refused. The container is probably listening on 127.0.0.1 rather than 0.0.0.0, so nothing outside the pod can reach it.
  • It works by pod IP but not by Service name. DNS. Check CoreDNS pods are running, then resolve from inside a pod rather than from a node.
  • Works in one namespace, not another. The short name only resolves within the namespace; use service.namespace across them.
  • Ingress has no address. No controller, or no ingressClassName in a cluster with no default.
  • Planning ranges. The CIDR calculator is quicker than doing the arithmetic twice.

Next steps

Deploy one application with a Deployment, a ClusterIP Service and an Ingress, then deliberately break the Service selector by one character and run the debugging sequence above. Doing it once when nothing is on fire is worth a lot when something is.

Enjoyed this article?

Get more DevOps insights delivered to your inbox.

Get new posts by email

Subscribe to get an email when a new blog post is published. Skip anytime.

No spam, unsubscribe anytime.

N

Written by

Neeraj Jha

Platform administrator and lead writer.

View all posts →
Terms used here (1)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Service Mesh
A dedicated infrastructure layer for managing service-to-service communication within a microservices architecture. Service meshes like Istio and Linkerd provide traffic management, mutual TLS, observability, and resilience features without changing application code.

All 59 terms in the glossary

Discussion

0 comments

Sign in to join the conversation.

Be the first to comment

Start a conversation about this post

Share: