Four Ground Rules: Order, Network, Names, Volumes
compose brings services up together according to dependencies, sharing one default network; services resolve each other by the service name, e.g. ping api, and a backend connects to the DB with host db.
The service name is the DNS name; do not reach for IPs. Expose a container service outward through ports host:container, while containers talk to each other on internal ports like 5432 or 3306 with no mapping.
Note depends_on only guarantees ordering of container start, not "the other service is ready". When the DB container is up but not yet accepting connections, the dependant may connect too early and fail — the classic integration hiccup.
services:
api:
image: node:20-alpine
ports:
- "8080:3000"
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
ports:
- "5432:5432"
ping-api-in-container:
image: alpine
command: sh -c "wget -qO- http://api:3000/health && echo OK"Start Order Is Not Readiness: Fixing First-Connect Failures
depends_on only guards creation order, so while the DB is “Created”, its socket is not listening yet, and the backend may hard-connect during a wrong retry window.
Three options: compose's own healthcheck with depends_on condition: service_healthy; an application-level connect-with-exponential-backoff; or a custom readiness script on the DB image. The first is recommended because it hands readiness semantics to the one who knows — the DB itself.
Give healthcheck a sane interval / timeout / retries and use a cheap probe so you are not slamming the local DB with a heavy query every five seconds.
services:
api:
image: node:20-alpine
environment:
DATABASE_URL: postgres://app:app@db:5432/app
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 3s
timeout: 3s
retries: 10
start_period: 10sPort Collisions, Leftover Containers, and Name Clashes
When system PostgreSQL already binds 5432, mapping the image's 5432 straight out fails with bind: address already in use. Either move the host port to 5433:5432 and update the app connection string, or stop the host service.
Leftover containers hold ports, names, or volumes, causing weird errors on up. Be consistent: docker compose down before up, and add -v if you truly want volumes gone too (careful, that deletes data).
Distinct service names let compose avoid clashes by naming project_service, but a manually docker run'd container with the same name can collide; and keep the compose file in version control so the team does not drift.
# 端口被占:改 host 端口映射
services:
db:
ports:
- "5433:5432" # host:5433 -> container:5432
# 应用同步
api:
environment:
DATABASE_URL: postgres://app:app@localhost:5433/app
# 彻底重启
# docker compose down
# docker compose up --buildMistake: A Dead Container Only Shows "cannot connect"
If you used the wrong image name, hit OOM, or a non-zero exit code, the failure lives outside the view of the connection query. What you see is the app saying connection refused while the real cause sits container-side.
Order your investigation: docker compose ps -a for exit codes; docker compose logs <service> for the app's own error; docker inspect <id> for ExitCode and OOMKilled; docker stats for memory if needed.
After checking names and ports, go deeper in two steps: docker compose exec <service> sh to probe inside (is it 127.0.0.1 or the db name), then compare against the compose mapping.
# 排查节奏
docker compose ps -a
docker compose logs --tail=100 api
docker inspect <container-id> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
# 进容器内部验证网络与端口
docker compose exec api sh
# inside: env | grep DATABASE_URL
# inside: exit
docker compose up --force-recreate --buildVerify Integration Truly Works: End-to-End
The ultimate check is a real request path: after startup, hit /health from another container, then walk a business query to confirm data lands.
You can run a throwaway alpine container attached to the same network and wget/curl each service name, testing without touching compose. Then run a real backend-calling script and inspect response codes and data.
Also add a host-side check: curl localhost:8080 from the host to confirm the mapping direction, so you are not tricked by a fake integration that works only inside the network.
# 一次性联调探测(复用同一网络)
docker run --rm --network <project>-default alpine sh -c \
"wget -qO- http://api:3000/health && echo; nc db 5432 < /dev/null && echo db-up"
# host 侧端口映射验证
curl -s http://localhost:8080/health
# 看最终日志是否整洁
docker compose logs --tail=20 db api