Docker · Lesson 4 of 11

Volumes, Bind Mounts and Networks

Persist container data with Docker volumes and bind mounts, and connect containers with user-defined bridge networks and DNS by container name.

  • Intermediate
  • 16 min read
  • 4 objectives

Before this lessonLesson 3: Writing a Dockerfile

What you will learn

  • Choose between named volumes, bind mounts and tmpfs
  • Back up and inspect a volume
  • Create a user-defined network and connect containers by name
  • Understand port publishing versus internal traffic

Your Progress

0 of 11 lessons 0%

  • Lessons0 / 11
  • Completed0
  • Est. time left~ 3 hours

Create a free account to keep your progress on every device.

Tip: pressing Next marks this lesson complete automatically.

Containers are designed to be disposable: you should be able to delete one and start a fresh copy at any moment. That raises two practical questions. Where does data go that must survive, like a database? And how do containers find each other without hard-coded IP addresses? Docker answers the first with storage mounts and the second with networks.

Here you will do it by hand with the plain docker CLI. In the next lesson, Docker Compose automates all of this, and you will know exactly what it is doing behind the scenes.

Three kinds of mounts

  • Named volume: storage managed by Docker (under /var/lib/docker/volumes on Linux). Best for database files and anything the app owns.
  • Bind mount: a folder or file from your machine mapped into the container. Best for local development (live code reload) and injecting a single config file.
  • tmpfs mount: in-memory only, gone when the container stops. Useful for scratch files or secrets you never want on disk.

The modern, explicit syntax is --mount. The short -v form still works and is what you will see in most tutorials; the two are equivalent.

Named volumes for data that must survive

docker volume create pgdata

docker run -d --name db \
  -e POSTGRES_PASSWORD=secret \
  --mount type=volume,source=pgdata,target=/var/lib/postgresql/data \
  postgres:16

# delete the container entirely...
docker rm -f db

# ...and the data is still there
docker volume ls
Output
DRIVER    VOLUME NAME
local     pgdata

Start a new postgres:16 container with the same volume and every table is still there. The container was replaced; the data was not. If you forget to create the volume first, Docker creates it automatically on docker run.

docker volume inspect pgdata
Output
[
    {
        "CreatedAt": "2026-09-20T10:14:02Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/pgdata/_data",
        "Name": "pgdata",
        "Options": null,
        "Scope": "local"
    }
]

Backing up a volume

Because a volume is just a directory, you can back it up by mounting it into a throwaway container alongside a bind mount of your current folder, then creating a tar archive:

docker run --rm \
  -v pgdata:/data:ro \
  -v "$PWD":/backup \
  alpine tar czf /backup/pgdata-2026-09-20.tgz -C /data .

ls -lh pgdata-2026-09-20.tgz
Output
-rw-r--r--  1 ada  staff    6.3M Sep 20 10:21 pgdata-2026-09-20.tgz

For a real database, prefer the database's own dump tool (pg_dump) while it runs, or stop the container before copying files, so you never archive a half-written state.

Bind mounts for development

A bind mount makes a host folder appear inside the container. Edit a file in your editor and the container sees the change instantly, which is perfect for hot reload:

docker run --rm -p 8000:8000 \
  --mount type=bind,source="$PWD",target=/app \
  -w /app python:3.12-slim \
  python -m http.server 8000

Add ,readonly (or :ro with -v) when the container should only read, such as mounting an nginx.conf. Two common gotchas: the source path must be absolute (hence $PWD), and files created by the container may be owned by root on your Linux host.

Networks: how containers find each other

Every container gets its own network namespace with its own localhost. That is why an app container cannot reach a database at localhost:5432: that address points back at the app container itself.

The fix is a user-defined bridge network. Containers on the same user-defined network can reach each other by container name, thanks to Docker's built-in DNS server. (The legacy default bridge network does not provide name resolution, which is a common source of confusion.)

docker network create stackcone-net

docker run -d --name db --network stackcone-net \
  -e POSTGRES_PASSWORD=secret -v pgdata:/var/lib/postgresql/data postgres:16

docker run --rm --network stackcone-net alpine ping -c 2 db
Output
PING db (172.19.0.2): 56 data bytes
64 bytes from 172.19.0.2: seq=0 ttl=64 time=0.112 ms
64 bytes from 172.19.0.2: seq=1 ttl=64 time=0.098 ms

--- db ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.098/0.105/0.112 ms

Your app would now use the connection string postgresql://postgres:secret@db:5432/postgres. Compose does exactly this: it creates a network named <project>_default and uses service names as hostnames.

Publishing ports versus internal traffic

Notice the database above has no -p flag. It does not need one: containers on the same network talk directly on the container port. You only publish a port (-p host:container) for traffic coming from outside Docker, such as your browser. Keeping databases unpublished is a simple, effective security win.

docker run -d --name api --network stackcone-net -p 127.0.0.1:8000:8000 stackcone-api:1.0.0
docker port api
Output
8000/tcp -> 127.0.0.1:8000

Binding to 127.0.0.1 makes the port reachable only from your own machine. Plain -p 8000:8000 binds to all interfaces, and on Linux Docker's firewall rules can expose it to the whole network even if you use ufw.

Recap

  • Use named volumes for data the app owns, bind mounts for development and config files, tmpfs for throwaway in-memory data.
  • Volumes outlive containers; back them up with a throwaway container or the database's dump tool.
  • Inside a container, localhost means that container only.
  • On a user-defined network, containers reach each other by name via Docker's DNS.
  • Publish ports only for traffic from outside Docker, and bind to 127.0.0.1 when possible.
# Write your solution here

Finished reading? Mark this lesson complete to track your progress.

Up next · Lesson 5Docker ComposeRun an app with a database and other services from one file.