Pods in Kubernetes receive replaceable IP addresses, so another workload should not connect to a Pod directly. A Service gives matching Pods a stable cluster DNS name and virtual IP, which is the usual internal entry point for an HTTP application, API, or backend listener.
A ClusterIP Service is the default Service type for traffic that stays inside the cluster. The Service selector must match the labels on the Pods, and the Service port must send traffic to the container port that the application actually listens on.
kubectl expose deployment copies the Deployment selector into the new Service and keeps the first Service object small enough to inspect. For source-controlled releases, save the resulting Service manifest with the workload YAML and review the selector before applying it to a shared namespace.
$ kubectl create namespace app-service namespace/app-service created
Use the target application namespace instead of app-service when exposing a real workload.
Related: How to create a Kubernetes namespace
$ kubectl create deployment web --namespace app-service --image=nginx:1.29.5-alpine --replicas=2 --port=80 deployment.apps/web created
The sample Deployment gives the Service ready Pods to select. Use the existing application Deployment when the workload is already running.
$ kubectl rollout status deployment/web --namespace app-service --timeout=180s Waiting for deployment "web" rollout to finish: 0 of 2 updated replicas are available... Waiting for deployment "web" rollout to finish: 1 of 2 updated replicas are available... deployment "web" successfully rolled out
$ kubectl expose deployment web --namespace app-service --name web --port=80 --target-port=80 --type=ClusterIP service/web exposed
--port is the Service port that clients use. --target-port is the container port behind the Service.
$ kubectl get service web --namespace app-service -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR web ClusterIP 10.96.194.197 <none> 80/TCP 10s app=web
The SELECTOR value must match labels on the Pods that should receive traffic.
$ kubectl get endpointslice --namespace app-service -l kubernetes.io/service-name=web NAME ADDRESSTYPE PORTS ENDPOINTS AGE web-v2dmg IPv4 80 10.244.0.3,10.244.0.2 10s
An empty ENDPOINTS column usually means the selector does not match ready Pods, the Pods are not ready, or the target port does not match the workload.
$ kubectl run service-check --namespace app-service --restart=Never --image=curlimages/curl:8.10.1 -- -sSI http://web pod/service-check created
$ kubectl wait pod/service-check --namespace app-service --for=jsonpath='{.status.phase}'=Succeeded --timeout=60s
pod/service-check condition met
$ kubectl logs service-check --namespace app-service HTTP/1.1 200 OK Server: nginx/1.29.5 Content-Type: text/html ##### snipped #####
A 200 OK response from http://web proves cluster DNS resolved the Service name and the Service routed traffic to a selected Pod.
$ kubectl delete namespace app-service namespace "app-service" deleted
Skip this command for a real application namespace. Delete only the test Service, Deployment, or request Pod if the namespace contains other workloads.