Full lesson
Explore the full explanation, examples, and visuals at your own pace.
Terminal output after deploying
Your API pod is crash-looping, while Postgres sits there running fine. Look at the log. The API tries localhost, port 5432, and gets connection refused. Inside that pod, localhost means the API's own pod, not the database. So the API is calling itself, and nothing there is listening.
$ kubectl get pods
NAME READY STATUS
api-7d9f8-x2k4q 0/1 CrashLoopBackOff
postgres-5c8b-9z 1/1 Running
$ kubectl logs deploy/api
Connecting to database at localhost:5432
Error: connect ECONNREFUSED 127.0.0.1:5432Setup 1: the laptop
On your laptop, this worked. The API process runs directly on the machine, so when it dials localhost on port 5432, localhost means the laptop itself. Postgres sits in a container, and a published port, which is a forwarding rule, sends that traffic from the machine into the Postgres container. One machine, one localhost.
Setup 2: Docker Compose
Docker Compose changes the picture. Each container gets its own localhost, so the API can't use it to find Postgres. Instead, the API dials the name postgres. Compose DNS, the built-in name lookup, resolves that name over the shared network to the Postgres container. Containers reach each other by service name.
Setup 3: Kubernetes, separate Pods
In Kubernetes, the API and Postgres run in separate Pods, and each Pod gets its own network namespace, a private view of the network. So when the API Pod dials localhost:5432, the call loops back inside that same Pod. Nothing is listening there, so the connection is refused.
Localhost: same Pod yes, different Pods no
Localhost depends on where the containers live. On the left, two containers in the same Pod share one network view, so the API can reach its helper through localhost. On the right, the API and Postgres sit in different Pods. Each has its own localhost, so that path is blocked.
The working path: a Service
Here's the path that works. The API Pod asks cluster DNS to resolve the name postgres, and DNS answers with the Service's IP address. The API then connects to that Service on port 5432. The Service is a stable address, so it forwards the traffic to the Postgres Pod's targetPort. The name you dial comes from the Service.
postgres-service.yaml
In this Service manifest, metadata name is what becomes the DNS name. So postgres comes from here, not from the Deployment or Pod name. The selector finds the Postgres Pods. Then there are two ports. The API dials port, and traffic goes to targetPort. They match here, but they can differ.
apiVersion: v1
kind: Service
metadata:
name: postgres # becomes the DNS name
namespace: data
spec:
selector:
app: postgres # matches the Postgres Pods
ports:
- port: 5432 # what the API dials
targetPort: 5432 # where the container listensapi-deployment.yaml (excerpt)
Now the API reads its database address from environment variables, DB_HOST and DB_PORT, instead of hardcoding localhost. The full name, postgres.data.svc.cluster.local, works from any namespace. Plain postgres resolves only for Pods inside the data namespace. Since the API runs elsewhere, use the full name.
containers:
- name: api
env:
- name: DB_HOST
value: postgres.data.svc.cluster.local
- name: DB_PORT
value: "5432"
# app reads DB_HOST and DB_PORT, no hardcoded localhostBefore and after deploying
Before you trust the fix, open a shell in a Pod and test that the Service name resolves and port 5432 answers. And don't put the API and database in one Pod just to keep localhost. They'd scale, restart, and be replaced together.
- Test DNS and port from inside a Pod
- Don't share a Pod just for localhost
- Same Pod means scaling and restarting together
Localhost always means this Pod; other Pods are reached through Services.
Localhost always means this Pod; other Pods are reached through Services. So set DB_HOST to the Service name, instead of hardcoding localhost, and let cluster DNS do the rest.
