Container-to-container DNS is the part people reach for Compose to avoid understanding. It is worth understanding: a user-defined bridge network gives every container on it a DNS name equal to its own name, and that single fact explains most of what a Compose file is doing for you.
1. Create a network and look at it
docker network create lab-net
docker network inspect lab-net \
--format '{{.Driver}} {{(index .IPAM.Config 0).Subnet}}'The default bridge network does not give you DNS between containers. A user-defined bridge does. This is the whole reason to create one.
Verify
2. Start Postgres with a named volume
docker run -d --name lab-db --network lab-net \
-e POSTGRES_PASSWORD=labpass \
-e POSTGRES_DB=labdb \
-v lab-db-data:/var/lib/postgresql/data \
postgres:16-alpineNo -p. Nothing is published to your host, because only the other container needs to reach it — an exposed database port is one of the most common ways a lab environment becomes an incident.
Wait for it to finish initialising:
docker exec lab-db pg_isready -U postgresVerify
3. Resolve the database by name from another container
docker run --rm -it --network lab-net postgres:16-alpine \
psql -h lab-db -U postgres -d labdb -c 'select version();'It will prompt for the password: labpass.
The host lab-db resolved to the other container. Nothing configured that — Docker's embedded DNS server answers for container names on a user-defined network. Prove that is what happened:
docker run --rm --network lab-net alpine:3.20 nslookup lab-dbVerify
Now show it fails on the default network:
docker run --rm alpine:3.20 nslookup lab-dbVerify
4. Write data, then destroy the container
docker exec -e PGPASSWORD=labpass lab-db \
psql -U postgres -d labdb \
-c "create table notes (body text)" \
-c "insert into notes values ('survived')"Delete the container entirely — not stop, delete:
docker rm -f lab-dbStart a new one, attached to the same volume:
docker run -d --name lab-db --network lab-net \
-e POSTGRES_PASSWORD=labpass \
-v lab-db-data:/var/lib/postgresql/data \
postgres:16-alpine
sleep 5
docker exec -e PGPASSWORD=labpass lab-db \
psql -U postgres -d labdb -c 'select * from notes'The row is there. The container was disposable; the volume was not. That is the line between the two, and it is the same line you will draw later between a stateless service and its managed database.
Verify
5. Confirm the isolation is real
The database is on lab-net and publishes nothing. From your host:
psql -h localhost -U postgres -d labdbRefused — there is no path from your host to that port. Only containers on lab-net can reach it.
Verify
Clean up
Order matters: containers, then the network, then the volume.
docker rm -f lab-db
docker network rm lab-net
docker volume rm lab-db-dataVerify
Where this goes next
You have a network you defined, name-based service discovery, and a volume that outlived its container. A Compose file writes those three things down; a Kubernetes Service and PersistentVolumeClaim are the same two ideas with more ceremony. The CI/CD track picks this up by building the service image in a pipeline and running these same containers as test dependencies.