1. Network Drivers
Every container connects to at least one Docker network, and the network's driver decides what isolation and connectivity it gets. Three drivers matter for local development:
bridge # DEFAULT. A private internal network on the host; containers get their own
# IP, can reach the internet via NAT, and reach each other only if explicitly
# allowed. This is what every "docker run" uses unless told otherwise.
host # No network isolation at all — the container shares the host's network
# stack directly. Fast (no NAT overhead), but zero port isolation: if your
# app binds port 3000, it's bound on the host, full stop.
none # No networking whatsoever. Used for batch jobs that only touch the
# filesystem and have no business making network calls.
docker network ls # you'll see "bridge" listed by default
docker run -d --name a alpine sleep 3600
docker run -d --name b alpine sleep 3600
docker exec a ping b # FAILS — no DNS resolution on the default bridge network
docker exec a ping $(docker inspect -f '{{.NetworkSettings.IPAddress}}' b) # works, by IP
That's the exercise from Week 1 explained: the default bridge network
(the one every plain docker run lands on) doesn't provide DNS between
containers — only raw IP connectivity, and IPs change every time a container
restarts. That limitation is exactly what Section 2 fixes.
Because it removes network isolation entirely, host mode also removes one of the main defenses a container gives you — a compromised process can now bind to any port on your actual host. Reach for it only for genuine performance-critical cases, and never as a default.
2. User-Defined Networks & DNS
Create your own bridge network, and Docker gives you something the default bridge doesn't: automatic DNS resolution by container name.
docker network create app-net
docker run -d --name a --network app-net alpine sleep 3600
docker run -d --name b --network app-net alpine sleep 3600
docker exec a ping b # WORKS — "b" resolves via Docker's embedded DNS server
docker exec b ping a # WORKS in both directions
Every user-defined bridge network runs an embedded DNS server that resolves
container names (and Compose service names, which you'll meet in Week 5) to their
current IP — automatically, even after a container restarts and gets a new IP.
This is the mechanism that makes "my API just calls http://database:5432"
work without ever hardcoding an IP address anywhere.
docker network ls # list all networks
docker network inspect app-net # see connected containers, subnet, gateway
docker network connect app-net some-other-container # attach an existing container
docker network rm app-net # delete a network (must have no containers attached)
Never rely on the default bridge network for anything beyond a single throwaway container. Real setups — even a two-container demo — should always create a user-defined network first. It's also the network isolation boundary: containers on different user-defined networks can't reach each other unless explicitly connected to both.
3. Port Publishing
Container-to-container traffic (Section 2) never needs a published port — it's all
internal to the Docker network. Port publishing (-p)
is a completely separate concern: exposing a container's port to the outside world
(your host machine, and beyond it, the internet).
docker run -p 8080:80 nginx
# ^host ^container
# "requests to host port 8080 get forwarded to container port 80"
docker run -p 127.0.0.1:8080:80 nginx
# bind ONLY to localhost — not reachable from other machines on the network
docker run -P nginx
# publish ALL exposed ports to random high host ports (rarely what you want)
A common point of confusion: EXPOSE in a Dockerfile is documentation
only — it doesn't actually publish anything. Only the -p flag at
docker run time creates a real host-to-container port mapping.
If your API and database talk to each other over a shared Docker network, the database never needs -p at all — publishing it just adds an unnecessary attack surface, exposing it to your whole host network for no benefit. Publish only what genuinely needs to be reached from outside Docker.
4. Wiring Two Containers Together, the Right Way
Putting Sections 1–3 together: an API and a database, on their own network, with only the API's port published.
docker network create backend-net
docker run -d --name db --network backend-net \
-e POSTGRES_PASSWORD=devpw -e POSTGRES_DB=appdb \
-v pg-data:/var/lib/postgresql/data \
postgres:16
# NOTE: no -p flag — the database is reachable only from other containers on backend-net
docker run -d --name api --network backend-net \
-e DATABASE_URL=postgres://postgres:devpw@db:5432/appdb \
-p 3000:3000 \
my-api:1.0
# The API connects using hostname "db" — resolved by Docker's embedded DNS —
# and its own port 3000 is the ONLY thing published to the host.
From the outside, only localhost:3000 exists. The database is
completely unreachable except through the API — the network topology itself
enforces that boundary, without a single firewall rule written by hand.
Writing raw docker network create and docker run commands like this works, but it doesn't scale past two or three containers. Week 5 replaces this entire section with a single declarative YAML file and one command — the underlying networking concepts you just learned are exactly what's happening under the hood.
5. Hands-on Exercise
Build a two-container app on a user-defined network, then prove the isolation
Wire together a real API and database on your own network, then deliberately try to break its isolation from the outside to confirm it's real.
Part 1 — Wire it together:
- Create a user-defined bridge network.
- Run a Postgres (or MySQL/MongoDB) container on it, with a named volume for persistence and no published port.
- Run an API container (your own image from Week 2, or any simple app that can connect to a database by hostname) on the same network, connecting to the database by its container name, and publish only the API's port.
- Confirm the whole thing works end-to-end from your browser or curl at
localhost:<api-port>.
If you don't have a real API on hand, an alpine container running apk add postgresql-client && psql -h db -U postgres is a fine stand-in — the point is proving hostname-based connectivity, not building a full API.
Part 2 — Prove the isolation from the outside:
- From your host machine (not inside any container), try to connect directly to the database's default port (5432 for Postgres) at
localhost. It should fail — there's nothing published there. - Run a THIRD container, on the default bridge network (don't specify
--network), and try to reach the database by its container name from inside it. Confirm it fails, and explain in one sentence why — tying it back to what you learned in Section 1 about the default bridge network. - Now connect that third container to your user-defined network too (
docker network connect), without restarting it, and retry the same hostname lookup. It should now succeed. - Run
docker network inspecton your user-defined network and identify the container names, IPs, and subnet it assigned — write down what you see.
A container can belong to more than one network at once — docker network connect adds a network without removing any existing ones, which is exactly why step 3 works without recreating the container.
6. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why can't two containers on the default bridge network reach each other by container name?
Why can't two containers on the default bridge network reach each other by container name?
The default bridge network doesn't run Docker's embedded DNS server for name resolution — only user-defined bridge networks do. On the default bridge, containers can only reach each other by raw IP address, which is also unreliable since IPs can change across restarts.
Q2
What's the difference between EXPOSE in a Dockerfile and -p at docker run?
What's the difference between EXPOSE in a Dockerfile and -p at docker run?
EXPOSE is documentation baked into the image — it doesn't publish anything by itself. Only -p host:container at run time actually creates a mapping from a host port to the container's port, making it reachable from outside Docker.
Q3
Why is it good practice to NOT publish a database container's port when only your API needs to reach it?
Why is it good practice to NOT publish a database container's port when only your API needs to reach it?
Container-to-container traffic on a shared Docker network never needs a published port — it's internal. Publishing the database's port anyway exposes it to the entire host network (and potentially beyond) for no functional benefit, unnecessarily increasing the attack surface.
Q4
What's the main tradeoff of using host network mode?
What's the main tradeoff of using host network mode?
Host mode removes network isolation entirely — the container shares the host's network stack directly, which avoids NAT overhead but means a compromised or misconfigured process can bind to any port on your actual host, eliminating one of a container's normal security boundaries.