Cloud AI Campus
  • Career paths
  • Learning paths
  • Hands-on Labs
Log in Sign up

๐Ÿงช Hands-on lab · 30 min

Docker โ€” Volumes & Storage

  1. 1. Why container data is ephemeral
  2. 2. Named volumes
  3. 3. Bind mounts
  4. 4. Inspect and clean up

Why container data is ephemeral

A container has two file layers:

  1. Image layers โ€” read-only, shared between containers from the same image.
  2. Container writable layer โ€” created on top when the container starts, destroyed when the container is removed.

Demo the problem:

# Start a container and write data to the scratchpad layer
docker run -d --name eph alpine sh -c "echo hello > /data && sleep 600"

# Verify the data exists (prints: hello)
docker exec eph cat /data

# Remove the container (discarding its writable layer)
docker rm -f eph

# Start a fresh container from the same base image
docker run -d --name eph alpine sh -c "sleep 600"

# Verify the data is gone (prints: No such file or directory)
docker exec eph cat /data 2>&1

Data didn't survive the container's death. That's by design โ€” the read-only base image is unchanged, the writable scratchpad is discarded. For anything you want to keep (databases, uploaded files, build caches) you need either a volume or a bind mount.

Click Verify step when you've reproduced the disappearance.

Hint

Containers are immutable; their writable layer is deleted with the container.

Named volumes

A named volume is managed by Docker on the host filesystem. It lives independently of any container โ€” containers come and go, volumes stay.

Create one + mount it into a fresh nginx:

docker volume create app-data
docker volume ls

docker run -d --name web \
    --mount source=app-data,target=/usr/share/nginx/html \
    -p 8300:80 \
    nginx:alpine

Write something into the volume via the container:

docker exec web sh -c 'echo "<h1>persisted</h1>" > /usr/share/nginx/html/index.html'
curl -s http://localhost:8300/

Now blow up the container โ€” but NOT the volume:

docker rm -f web

Start a brand-new container, mount the same volume:

docker run -d --name web2 \
    --mount source=app-data,target=/usr/share/nginx/html \
    -p 8301:80 \
    nginx:alpine

curl -s http://localhost:8301/

You should see persisted โ€” the new container reads the same files the previous one wrote. That's the volume earning its keep.

Volumes also support the older -v syntax โ€” same effect, terser:

# docker run -d -v app-data:/usr/share/nginx/html nginx:alpine

Click Verify step.

Hint

`docker volume create`, then `--mount source=<vol>,target=<path>` at run.

Bind mounts

A bind mount mounts a specific host directory into the container. Unlike named volumes, the host path is literal and visible โ€” useful for dev (edit host files, see them in the container immediately).

Create a host directory with content:

mkdir -p ~/site
cat > ~/site/index.html <<'HTML'
<!doctype html>
<html><body><h1>served from a bind mount</h1></body></html>
HTML

Mount it at /usr/share/nginx/html:

docker run -d --name dev-web \
    -v ~/site:/usr/share/nginx/html:ro \
    -p 8310:80 \
    nginx:alpine

curl -s http://localhost:8310/

Now edit the host file โ€” the container immediately serves the new content:

echo "<h1>edited live</h1>" > ~/site/index.html
curl -s http://localhost:8310/

The :ro flag mounts read-only. That's the safer default for shipping config into a container โ€” code in the container can't accidentally overwrite your host files.

Volumes vs bind mounts โ€” when to use which:

| | Volume | Bind mount | |--|--|--| | Path | Docker manages | You pick | | Portable | Yes | Tied to host paths | | Backups | docker run --rm -v vol:/x alpine tar ... | Just tar the host dir | | Best for | Databases, caches, app data | Local dev hot-reload, config files |

Click Verify step when :8310 serves "edited live".

Hint

`-v $(pwd):/app` mounts a host path. Read-only is `:ro`.

Inspect and clean up

A few daily-driver storage commands.

List + inspect:

docker volume ls
docker volume inspect app-data
docker volume inspect --format '{{ .Mountpoint }}' app-data

The mountpoint is the real directory on the host. For operational debugging you can ls it directly:

ls $(docker volume inspect --format '{{.Mountpoint}}' app-data)

Tag volumes with labels so prune doesn't kill them:

docker volume create --label keep=true important-data
docker volume ls --filter label=keep

Prune โ€” remove unused volumes:

docker volume prune -f
docker volume ls

prune deletes only dangling volumes (no container is mounting them). To wipe a specific one:

docker volume rm important-data

If you try to remove a volume that's still in use, you get an explicit error naming the using container.

Click Verify step when app-data still exists and contains the persisted content from step 2.

Hint

`docker volume ls`, `docker volume inspect`, `docker volume prune`.

© 2026 Cloud AI Campus