Volumes & Networking
A container's filesystem dies with the container, and its network is walled off from everything else. Both are features — disposability and isolation are the point — but real apps need two controlled exceptions: data that survives (a database's files) and traffic that gets in (users reaching your web server). Volumes handle the first, port mapping the second.
💡 The Bottom Line
A volume mounts storage that lives outside the container's lifecycle,
so data survives stop, delete, and re-create. Port mapping
(-p host:container) opens a specific door from the host machine into the
container's isolated network. Everything else stays disposable and sealed.
Named volumes: let Docker manage the storage
docker volume create pgdata
docker run -d --name db \
-e POSTGRES_PASSWORD=secret \
-v pgdata:/var/lib/postgresql/data \
postgres:16
-v pgdata:/var/lib/postgresql/data says: mount the volume named
pgdata at the path where Postgres keeps its data. Now
docker rm -f db destroys the container, but running the same command again picks
up the exact same data — the volume outlived it. This is the standard pattern for databases
and anything stateful.
Bind mounts: use a folder you can see
docker run -d --name web -p 8080:80 \
-v "$(pwd)/site":/usr/share/nginx/html \
nginx:1.27
A bind mount maps a real host folder into the container. Edit
site/index.html in your editor and refresh the browser — the change is live,
no rebuild. That makes bind mounts perfect for local development; named volumes are usually
the better choice for real data, since Docker manages their location and lifecycle.
Ports: opening the one door you need
By default nothing can reach a container from outside. -p 8080:80 publishes
container port 80 onto host port 8080 — the two numbers don't have to match, which is how you
run three web containers side by side on ports 8080, 8081, and 8082, each thinking it owns
port 80.
Containers can also talk to each other — put them on the same user-defined network and Docker gives them DNS by container name:
docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name api --network appnet -p 3000:3000 myapp:1.0
# inside "api", the database is reachable as host "db" — no IP needed
🙋 There Are No Dumb Questions
Q: Why can't my API container reach the database at localhost:5432?
A: Because inside a container, localhost means that container itself —
not your machine, and not other containers. Each container has its own network view.
Put both containers on a shared network and use the container's name as the
hostname (db:5432). This one misunderstanding explains most "connection
refused" errors beginners hit.
⚠️ Watch Out
"Port is already allocated" means the host port is taken — some other process
(or another container) already claimed it. Only one thing can own host port 8080 at a time.
Pick a different host-side number (-p 8081:80); the container-side port stays
whatever the app listens on.
🔧 Try It Yourself
Prove that volumes survive container deletion:
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16
docker exec -it db psql -U postgres -c "CREATE TABLE t (msg text); INSERT INTO t VALUES ('still here');"
docker rm -f db
docker run -d --name db -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16
docker exec -it db psql -U postgres -c "SELECT * FROM t;"
The row is still there — the container died, the volume didn't.
✅ Quick Check
In docker run -p 8080:80 nginx, what do the two numbers mean?
Next up: your app is now several containers with volumes, networks, and ports — time to stop typing all those flags and describe the whole stack in one file with Docker Compose.