Part 7 separated dev and prod containers. Now we map how those containers actually communicate. The mission is simple: build a clear picture of host ports, container ports, and service names. The example stack uses three services: nginx web, Node API, and PostgreSQL DB.
How this post flows
- Draw a mental diagram that separates host, container, and service names
- Read a web + API + DB Compose snippet for ports and networks
- Understand how service-name DNS works inside Compose
- Mini Lab: confirm each path with success and failure cases
- Summarize the addresses people mix up most often
Reading card
- Estimated time: 17 minutes
- Prereqs:
docker compose upand basic port concepts- After reading: you can explain port mappings and service names to a beginner.
Mental diagram
Keep the following text diagram in mind and confusion disappears.
[Browser] --localhost:8080--> [Host OS] --port mapping--> [web container 80]
|
+--> other containers on the same network (service name)
- The browser always connects with localhost + the published port.
- Port mappings follow the
ports: "8080:80"pattern (host:container). - Containers on the same network resolve service names (
api,db, ...) to each other through internal DNS.
Separating those areas keeps 8080 vs. 80 or api:4000 vs. localhost:4000 from blurring together.
Before reading the Compose file, keep one lookup rule nearby: use localhost from the host, and use service names from one container to another.
| Your location | Target | Address |
|---|---|---|
| Browser or host terminal | web | http://localhost:8080 |
| Host terminal | API | http://localhost:4000 |
web container |
API | http://api:4000 |
api container |
DB | db:5432 |
Web + API + DB Compose snippet
Here is the same diagram written as Compose. Watch how the ports and shared network encode those paths.
services:
web:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./public:/usr/share/nginx/html:ro
depends_on:
- api
networks:
- app-net
api:
build: ./api
environment:
- DATABASE_URL=postgres://postgres:your-db-password@db:5432/app
ports:
- "4000:4000"
networks:
- app-net
db:
image: postgres:16
environment:
- POSTGRES_PASSWORD=your-db-password
- POSTGRES_DB=app
volumes:
- db-data:/var/lib/postgresql/data
networks:
- app-net
volumes:
db-data:
networks:
app-net:
driver: bridge
webexposes host8080→ container80and serves static files read-only.apikeeps4000:4000open so you can debug it from the host.dbexposes no host port at all, so only containers on the same network can reachdb:5432.DATABASE_URLcontainsdb:5432. Heredbis the service name, and Compose's internal DNS resolves it on the shared network.depends_onstartsapibeforeweb, but it does not guarantee that the API is ready to answer requests yet. Part 9 covers that gap with health checks.
In a real project, make sure the API server listens on 0.0.0.0 inside the container. If it listens only on 127.0.0.1 (localhost), requests arriving through the container network interface will miss it.
Service names act as internal DNS
That db value in DATABASE_URL is the important clue. Compose provides internal DNS on the shared project network, so the service name becomes the address other containers use.
Compose creates a DNS record for each service name on every shared network. Within one network you can reach another container with service-name:port.
| Goal | Address |
|---|---|
| View the web app from the browser | http://localhost:8080 |
| Call the API from the host | http://localhost:4000 |
| Web container calling the API | http://api:4000 |
| API container connecting to the DB | postgres://...@db:5432/app |
localhost always points to the machine or container you are currently inside. Containers use service names to find each other. Confusing those two namespaces creates most networking bugs.
Therefore: use localhost on the host, use service names inside the Compose network. The host cannot normally resolve api, because that DNS name exists only inside the Compose network.
Mini Lab: trace every hop
- Save the Compose file and run
docker compose up -d. curl localhost:8080to confirm the static site loads.curl localhost:4000/healthto confirm the API responds.- Intentionally try the wrong address from inside
web:docker compose exec web sh -c "wget -qO- http://localhost:4000/health". - That request should fail, because
localhostinsidewebpoints back to thewebcontainer itself. - Try the correct address instead:
docker compose exec web sh -c "wget -qO- http://api:4000/health". - Test DB readiness from inside
dbwithdocker compose exec db pg_isready -U postgres -d app. - Run
docker compose downto clean up.
Repeating those steps makes it easy to narrate “I am connecting from X to Y through Z.”
Common mistakes are memorable too:
- Running
docker compose exec web ... http://localhost:4000/healthfails becauselocalhostinsidewebisweb's own loopback interface. The request never leaves that container. - Using
http://api:4000/healthinside the same network succeeds because Compose resolvesapito the API container's network address.
Inside any container, localhost refers to that container, not your Mac. Keep the scopes straight:
localhostinside a container: loopback of that container- Talking to another container: use the service name on the shared network
- Talking back to the host: on Docker Desktop, use names like
host.docker.internal; on Linux, that name is not available by default
Quick lookup for the most confusing addresses
- Browser → web:
http://localhost:8080 - Web container → API:
http://api:4000 - API container → DB:
db:5432
Remember the mantra: localhost for the host, service names for container-to-container traffic. Part 9 explains how health checks and restart policies keep these communication paths alive.
💬 댓글
이 글에 대한 의견을 남겨주세요