By default, Docker creates a bridge network and attaches every container to it unless told otherwise. Two things follow from this:
Ports aren't reachable from outside the host unless published. EXPOSE in a Dockerfile is documentation — it doesn't open anything. Publishing happens at docker run time:
docker run -p 8080:80 nginx
# host:container — traffic to localhost:8080 is forwarded to port 80 inside the container
Containers on the same user-defined network can reach each other by container/service name, without publishing anything to the host at all — Docker runs an embedded DNS server that resolves container names to their internal IPs on that network:
docker network create app-net
docker run --network app-net --name db postgres
docker run --network app-net --name web myapp
# from inside "web", the hostname "db" resolves to the db container's address
This matters because the default bridge network (the one containers land on if you don't create your own) does not provide this name resolution — only user-defined bridge networks do. A common beginner confusion is expecting --link-style or hostname-based container-to-container communication to "just work" without ever creating a network; Docker Compose (below) creates a user-defined network for you automatically, which is part of why it feels like container names "just work" there but not with bare docker run on the default network.