Containers are isolated by design — no network access and no persistent storage unless you explicitly grant it. Understanding exactly what Docker's default network and default filesystem give you (and don't) is the difference between 'why can't my app reach the database' and 'why did I just lose my database.'
3 min read
A container gets its own network namespace and its own filesystem view, separate from the host and from other containers, unless something explicitly connects them. This is a feature — it's what makes two containers running conflicting versions of the same library coexist safely on one machine — but it means "make container A talk to container B" and "keep this data after the container is removed" are both things you have to set up on purpose.
docker run without any network flag attaches a container to Docker's default bridge network — containers on it get private IPs and can reach the internet outbound, but by default they can only reach each other by IP, not by name, and a container's IP changes every time it's recreated.
A user-defined bridge network (what docker compose creates automatically for you) fixes this by adding built-in DNS: containers on the same user-defined network can resolve each other by container/service name, not just IP.
docker network create my-app-net
docker run -d --name db --network my-app-net postgres:16
docker run -d --name app --network my-app-net \
-e DATABASE_URL=postgresql://db:5432/myapp my-app:latestInside app, connecting to host db resolves correctly — Docker's embedded DNS server handles it. This is the same mechanism Compose relies on, just done manually.
-p vs EXPOSEEXPOSE 3000 in a Dockerfile is documentation only — it doesn't actually make the port reachable from outside the container. Publishing a port to the host requires -p (or Compose's ports:) at run time:
docker run -p 3000:3000 my-app:latest
# ^host ^container-p 8080:3000 maps host port 8080 to the container's port 3000 — the two numbers don't have to match, and mixing them up (publishing the wrong side) is a common source of "it works but I can't reach it from my browser" confusion.
A container's own filesystem is ephemeral — every write inside the container disappears the moment the container is removed (docker rm), even though it survives a simple stop/start. For anything that must outlive the container — a database's data files, user-uploaded content — that write has to land somewhere outside the container's own writable layer.
Docker gives two main options:
Named volumes — Docker manages the storage location; you refer to it by name:
docker volume create db-data
docker run -v db-data:/var/lib/postgresql/data postgres:16Portable, easy to back up (docker volume commands work regardless of where Docker actually stores the data on the host), and the standard choice for a database's data directory.
Bind mounts — you mount a specific host directory directly into the container:
docker run -v /home/dev/my-app/src:/app/src my-app:latestUseful for local development (edit code on the host, see changes live inside the running container without rebuilding) but ties the container to a specific host path — not portable across machines, and not the right choice for production data.
docker compose down -v deletes your data on purposedocker compose down alone stops and removes containers but leaves named volumes intact — the next docker compose up reattaches to the same data. Adding -v explicitly removes volumes too. This distinction matters in practice: a developer troubleshooting a stuck stack who reaches for docker compose down -v "just to reset everything" has also just deleted their local database's contents, which is usually fine in dev and catastrophic if that same habit or script ever gets run somewhere it shouldn't.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why can't two containers on Docker's default bridge network reach each other by container name?
2. Does 'EXPOSE 3000' in a Dockerfile make the container's port 3000 reachable from the host?
3. What is the key difference between a named volume and a bind mount?
4. What does 'docker compose down -v' do that plain 'docker compose down' does not?
Docker & Containers