Advanced Port Mapping

Introduction: Real-World Port Mapping Challenges

In the previous lesson, you learned how NodePort Services expose applications externally using a three-port system: targetPort (where your container listens), port (for internal cluster access), and nodePort (for external access). Those examples used single containers with single ports, which works great for simple applications. However, real-world applications are rarely that simple. You might run a web server alongside a metrics exporter in the same Pod, or expose both an HTTP API and an admin interface on different ports. You might need to standardize your Service ports across different applications while each application uses different internal ports.

This lesson teaches you advanced port mapping patterns that handle these complex scenarios. You'll learn how to expose multiple ports from multi-container Pods, use named port references to decouple configuration from implementation, and debug port misconfigurations when things don't work as expected. By the end, you'll be able to design flexible Service architectures that support sidecars, observability tools, and multiple protocols running together.

Multi-Container Pods with Multiple Ports

Let's start with a common real-world scenario: you have a web application that needs to expose both its main HTTP interface and a metrics endpoint for monitoring. The best practice is to run these as separate containers in the same Pod — your main application container and a sidecar container that exports metrics. Each container listens on its own port, and you need a Service that can route traffic to both ports. This is where multi-port Services come in. A multi-port Service defines multiple port entries, each with its own targetPort that routes to a specific container in your Pods.

Let's build this step by step. First, we'll create a Pod with two containers: an nginx web server and a Prometheus node exporter for metrics. The nginx container will listen on port 80, and the metrics exporter will listen on port 9100. Save this as deployment-multi-container.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: multi-container-pod
  labels:
    app: multi-demo

We're creating a Pod (not a Deployment) to keep the example simple, but the same principles apply to Deployments. The app: multi-demo label is crucial — our Service will use this to find the Pod.

spec:
  containers:
    - name: web
      image: nginx:1.25
      ports:
        - containerPort: 80

The first container runs nginx on port 80. The containerPort: 80 declaration tells Kubernetes that this container exposes port 80. While this declaration is technically optional for numeric port routing (the container will listen on port 80 regardless), it's a best practice because it documents which ports your container uses and enables named port references.

    - name: metrics
      image: prom/node-exporter:v1.6.1
      ports:
        - containerPort: 9100

The second container runs the Prometheus node exporter, which exposes system metrics on port 9100 by default. Now we have a Pod with two containers listening on two different ports. Let's create it:

kubectl apply -f deployment-multi-container.yaml
pod/multi-container-pod created

Let's verify both containers are running:

kubectl get pod multi-container-pod
NAME                  READY   STATUS    RESTARTS   AGE
multi-container-pod   2/2     Running   0          15s

The READY column shows 2/2, confirming both containers are running.

Creating a Multi-Port Service

Now we need a Service that can route traffic to both ports. This is where the multi-port configuration comes in. Save this as service-multi-port.yaml:

apiVersion: v1
kind: Service
metadata:
  name: multi-port-svc
  labels:
    app: multi-demo
spec:
  type: ClusterIP
  selector:
    app: multi-demo

This looks like a standard Service so far. The selector matches our Pod's app: multi-demo label, so the Service will route traffic to our multi-container Pod. We're using the ClusterIP type, but the same multi-port pattern works with NodePort and LoadBalancer types too.

  ports:
    - name: http
      port: 8080
      targetPort: 80
    - name: metrics
      port: 9091
      targetPort: 9100

Here's the key difference: we're defining two port entries instead of one. Each entry has a name (required when you have multiple ports), a port (what clients connect to), and a targetPort (which container port to route to). The first entry named http listens on port 8080 and routes to port 80 (the nginx container). The second entry named metrics listens on port 9091 and routes to port 9100 (the metrics exporter container). When you have multiple ports, Kubernetes requires you to name each one so you can reference them unambiguously.

Let's create the Service:

kubectl apply -f service-multi-port.yaml
service/multi-port-svc created

Now let's verify the Service configuration:

kubectl describe service multi-port-svc
Name:              multi-port-svc
Namespace:         default
Labels:            app=multi-demo
Selector:          app=multi-demo
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.45.67
IPs:               10.96.45.67
Port:              http  8080/TCP
TargetPort:        80/TCP
Endpoints:         10.244.0.8:80
Port:              metrics  9091/TCP
TargetPort:        9100/TCP
Endpoints:         10.244.0.8:9100
Session Affinity:  None
Events:            <none>

Look at the output carefully. You see two Port entries: one for http (808080) and one for metrics (90919100). Each has its own Endpoints entry showing the Pod IP with the corresponding target port. The same Pod IP (10.244.0.8 in this example) appears twice because both containers are in the same Pod, but the ports are different (80 and 9100). This confirms the Service is correctly routing to both containers.

Testing Multi-Port Service Connectivity

Let's test each port independently. First, let's test the web server:

kubectl port-forward service/multi-port-svc 8080:8080
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

In a new terminal, test the HTTP endpoint:

curl http://localhost:8080/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

Perfect! The web server is accessible. Now stop the port-forward (Ctrl+C) and test the metrics port:

kubectl port-forward service/multi-port-svc 9091:9091
Forwarding from 127.0.0.1:9091 -> 9100
Forwarding from [::1]:9091 -> 9100

In a new terminal, test the metrics endpoint:

curl http://localhost:9091/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
...

Excellent! You're seeing Prometheus metrics from the node exporter. This demonstrates that a single Service can route to multiple containers in the same Pod, each listening on different ports. This pattern is essential for sidecars, where you run supporting containers (like metrics exporters, log shippers, or proxies) alongside your main application container.

Defining Named Ports in Containers

In the previous section, we used hardcoded port numbers like targetPort: 80 and targetPort: 9100. This works, but it creates a tight coupling between your Service configuration and your container implementation. If you decide to change the port your application listens on, you have to update both the Deployment (where the container is defined) and the Service (where the targetPort is specified). This becomes error-prone as your application grows. Named ports solve this problem by letting you assign a name to a port in your container spec, then reference that name in your Service. This way, if you change the actual port number, you only update it in one place — the container spec — and the Service continues to work.

Let's see this in action. We'll create a Deployment where the container defines a named port, then create a Service that references that port by name instead of by number. Save this as deployment-named-port.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-named-port
  labels:
    app: named-port-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: named-port-demo

This is a standard Deployment with two replicas. The app: named-port-demo label will be used by our Service selector.

  template:
    metadata:
      labels:
        app: named-port-demo
    spec:
      containers:
        - name: app
          image: nginx:1.25
          ports:
            - name: http-web
              containerPort: 80

Here's the important part: instead of just declaring containerPort: 80, we're also giving it a name: http-web. This name is arbitrary — you can call it whatever makes sense for your application. The key is that this name becomes a stable reference point. We're using nginx which listens on port 80 by default, and we're giving that port a meaningful name. In production applications, you might have containers that listen on non-standard ports (like 8080, 3000, or 5000), and naming those ports makes your configuration more maintainable.

Let's create the Deployment:

kubectl apply -f deployment-named-port.yaml
deployment.apps/app-named-port created

Verify the Pods are running:

kubectl get pods -l app=named-port-demo
NAME                              READY   STATUS    RESTARTS   AGE
app-named-port-7d8f9c5b4d-abc12   1/1     Running   0          10s
app-named-port-7d8f9c5b4d-def34   1/1     Running   0          10s

Referencing Named Ports in Services

Now let's create a Service that references the port by name. Save this as service-named-port.yaml:

apiVersion: v1
kind: Service
metadata:
  name: named-port-svc
  labels:
    app: named-port-demo
spec:
  type: ClusterIP
  selector:
    app: named-port-demo

Standard Service metadata and selector. Nothing special here.

  ports:
    - name: http
      port: 8080
      targetPort: http-web

Here's where named ports shine: instead of targetPort: 80, we're using targetPort: http-web. This references the port name we defined in the container spec. Kubernetes will look at the Pods matched by the selector, find the port named http-web, and route traffic to whatever port number that name corresponds to. If we later decide to change the container to listen on port 9000 instead of 80, we only need to update the containerPort in the Deployment. The Service configuration stays exactly the same because it references the name, not the number.

Let's create the Service:

kubectl apply -f service-named-port.yaml
service/named-port-svc created

Let's verify the Service found the correct port:

kubectl get endpoints named-port-svc
NAME             ENDPOINTS                           AGE
named-port-svc   10.244.0.9:80,10.244.0.10:80       15s

Perfect! The endpoints show port 80, which is the actual port number from our container spec. The Service successfully resolved the http-web name to port 80. Let's test connectivity:

kubectl port-forward service/named-port-svc 8080:8080
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

In a new terminal:

curl http://localhost:8080/
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

It works! The Service is routing to the named port correctly.

Demonstrating Named Port Flexibility

Now let's demonstrate the power of named ports. Imagine you need to change your application to listen on port 8080 instead of 80. With hardcoded port numbers, you'd have to update both the Deployment and the Service. With named ports, you only update the Deployment. Let's simulate this by editing the Deployment:

kubectl edit deployment app-named-port

Find the containerPort section and change it from 80 to 8080:

ports:
  - name: http-web
    containerPort: 8080

Save and exit. Kubernetes will roll out new Pods with the updated port. Wait a moment for the rollout to complete, then check the endpoints again:

kubectl get endpoints named-port-svc
NAME             ENDPOINTS                           AGE
named-port-svc   10.244.0.11:8080,10.244.0.12:8080   2m

The endpoints now show port 8080! The Service automatically picked up the change because it references the port by name, not by number. You didn't have to touch the Service configuration at all. This is the key benefit of named ports: they decouple your Service configuration from your container implementation, making your application more maintainable and less error-prone.

One common mistake to watch out for: the port name in your Service's targetPort must exactly match the port name in your container spec. If you have name: http-web in the container but targetPort: http-app in the Service, Kubernetes won't be able to resolve the port and your Service will fail. Always double-check that the names match exactly, including capitalization and hyphens.

Port Mapping Patterns

Now that you understand multi-port Services and named ports, let's explore some practical patterns. One common pattern is standardizing Service ports across different applications while allowing each application to use whatever internal port makes sense. For example, you might decide that all HTTP services in your cluster should expose port 80 on their Service, regardless of what port the actual container listens on. This makes it easier for other services to connect — they always use port 80, and the Service handles the translation to the correct container port.

Let's say you have three different applications: one listens on port 8080, one on port 3000, and one on port 5000. You can create Services for all three that expose port 80, using targetPort to route to the correct container port. Here's what that looks like conceptually:

# Service for app listening on 8080
ports:
  - port: 80
    targetPort: 8080

# Service for app listening on 3000
ports:
  - port: 80
    targetPort: 3000

# Service for app listening on 5000
ports:
  - port: 80
    targetPort: 5000

All three Services expose port 80, so other Pods can connect to app1-svc:80, app2-svc:80, and app3-svc:80 without needing to know the internal port numbers. This standardization makes your service mesh more consistent and easier to understand. You can combine this with named ports for even more flexibility — expose port 80 on the Service, but use targetPort: http to reference a named port in the container.

Another useful pattern is following protocol-specific port conventions. The Kubernetes community has established some informal standards: port 8080 for HTTP services, port 9090 for metrics endpoints, port 8443 for HTTPS, and various high ports for admin interfaces. While these aren't enforced, following them makes your cluster more intuitive. When someone sees a Service exposing port 9090, they immediately know it's probably a metrics endpoint. When they see port 8080, they know it's likely an HTTP API. This consistency reduces cognitive load and makes troubleshooting easier.

Supporting Multiple Transport Protocols

Some applications need to handle both TCP and UDP traffic on the same port number. DNS servers are a perfect example — they typically listen on port 53 for both TCP (used for large queries and zone transfers) and UDP (used for standard queries). By default, Kubernetes Services use the TCP protocol, but you can explicitly specify the protocol using the protocol field in your port configuration. When you need to expose the same port number with different protocols, you create separate port entries, each with its own protocol field:

apiVersion: v1
kind: Service
metadata:
  name: dns-svc
spec:
  selector:
    app: dns-server
  ports:
    - name: dns-tcp
      port: 53
      targetPort: 53
      protocol: TCP
    - name: dns-udp
      port: 53
      targetPort: 53
      protocol: UDP

Notice that both entries use the same port number (53) and the same targetPort (53). The only difference is the protocol field. This is different from the multi-port examples we saw earlier, where we used different port numbers for different services. Here, we're using the same port number but different transport protocols. The protocol field accepts two values: TCP (the default) and UDP. Most HTTP/HTTPS services use TCP because it provides reliable, ordered delivery. UDP is used when you need lower latency and can tolerate occasional packet loss, like DNS queries, real-time gaming, or voice/video streaming. When working with protocols, remember that your container must actually listen on both protocols — declaring them in the Service won't make a TCP-only application suddenly accept UDP traffic.

When should you use explicit port numbers versus named port references? Use explicit numbers when the port is unlikely to change and you want the configuration to be immediately clear. For example, if you're exposing a standard protocol like HTTP on port 80 or HTTPS on port 443, hardcoding those numbers is fine — everyone knows what they mean. Use named ports when you want flexibility to change the implementation without updating multiple files, or when the port number is application-specific and might vary between environments. Named ports are especially valuable in large applications with many services, where keeping port numbers synchronized across Deployments and Services becomes a maintenance burden.

Debugging Port Configuration Issues

When a Service isn't working, the problem is usually one of three things: wrong targetPort, selector mismatch, or the container isn't actually listening where you think it is. Here's a systematic debugging workflow. First, describe the Service to verify its configuration:

kubectl describe service your-service-name

Look at the Port, TargetPort, and Endpoints sections. The Port should be what you expect clients to connect to. The TargetPort should match either a port number or a port name from your container spec. The Endpoints should show one or more IP:port combinations. If Endpoints is empty, your selector isn't matching any Pods. If Endpoints shows the wrong port number, your targetPort is incorrect.

Let's simulate a common mistake: a Service with the wrong targetPort. Create a Service that tries to route to port 9999, which doesn't exist:

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
  name: broken-svc
spec:
  selector:
    app: named-port-demo
  ports:
    - port: 80
      targetPort: 9999
EOF
service/broken-svc created

Now describe it:

kubectl describe service broken-svc
Name:              broken-svc
Namespace:         default
Selector:          app=named-port-demo
Type:              ClusterIP
IP:                10.96.78.90
Port:              <unset>  80/TCP
TargetPort:        9999/TCP
Endpoints:         10.244.0.11:9999,10.244.0.12:9999
Session Affinity:  None
Events:            <none>

The Endpoints show port 9999, but if you try to connect, it will fail because the containers aren't listening on that port. Let's test it:

kubectl port-forward service/broken-svc 8080:80
Forwarding from 127.0.0.1:8080 -> 9999
Forwarding from [::1]:8080 -> 9999

In a new terminal:

curl http://localhost:8080/
curl: (52) Empty reply from server

The connection fails because port 9999 isn't open on the Pods. This is a classic symptom of a wrong targetPort. To fix it, you'd update the Service to use the correct port (80 in our case, or the named port http-web).

Another common issue is forgetting to declare containerPort when using named ports. If your Service references a port name (like targetPort: http-web) but the Pod spec doesn't declare a port with that name, Kubernetes can't resolve the reference and the Service will fail. Note that for numeric targetPort values (like targetPort: 80), the containerPort declaration is not required for routing to work — Kubernetes will route to that port number regardless of what's declared in the Pod spec. However, declaring containerPort is still a best practice because it documents your container's network interface and enables named port references.

If your Service has no endpoints at all, the problem is usually a selector mismatch. Check that the Service's selector exactly matches the labels on your Pods. Use kubectl get pods --show-labels to see what labels your Pods actually have, then compare them to your Service selector. Even a small typo like app: demo versus app: demos will cause the selector to fail.

When debugging, the kubectl get endpoints command is your best friend. It shows you exactly which Pod IPs and ports the Service is routing to. If the endpoints are empty, your selector is wrong. If the endpoints show the wrong port, your targetPort is wrong. If the endpoints are correct but connections still fail, the problem is likely in your container — maybe it's not actually listening on the port you think it is, or maybe there's a firewall rule blocking the connection.

Summary: Building Flexible Service Architectures

You've now mastered advanced port mapping patterns that enable complex application architectures in Kubernetes. You learned how to expose multiple ports from multi-container Pods using multi-port Services, where each port entry routes to a different container. You discovered how named ports decouple your Service configuration from container implementation, making your applications more maintainable by letting you change port numbers in one place. You explored practical patterns like standardizing Service ports across applications and following protocol-specific conventions for consistency.

Finally, you learned a systematic debugging workflow for troubleshooting port configuration issues using kubectl describe, kubectl get endpoints, and kubectl port-forward. These skills prepare you to design Services for real-world microservices architectures with sidecars, observability tools, and multiple protocols running together. In the upcoming practice exercises, you'll create your own multi-container setups, experiment with named port references, and debug intentional misconfigurations to solidify your understanding.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal