Bind Mounts, Read-Only Mounts, tmpfs, Mount Propagation, Subpaths, SELinux Labels, and Host Coupling: Guided Hands-On Workflow and Core Operations
Compare read-only and writable bind mounts, strict versus auto-created sources, tmpfs, mount inspection, subpath behavior, and platform-specific security constraints.
Learning objectives
- Create a disposable host directory and prove the difference between read-only and writable bind authority.
-
Compare strict
--mountsource handling with-vauto-creation behavior and verify the result. - Create and inspect a bounded tmpfs mount and prove its data disappears with the container.
- Demonstrate mount-type-specific subpath behavior without implying undocumented bind-subpath support.
- Explain SELinux and propagation constraints without weakening host security.
./da21-lab beneath
the current working directory, BusyBox, and exact container/volume
names beginning da21-. Do not mount host root, Docker
control sockets, credential directories, or production paths. No
daemon setting, security policy, or firewall setting is changed.
1. Preflight and workspace
docker version
docker info
docker context show
docker compose version || true
docker buildx version || true
docker ps -a --no-trunc
docker volume ls
docker pull busybox:1.36.1
mkdir -p da21-lab/host-ro da21-lab/host-rw da21-lab/evidence
printf 'academy-readonly
' > da21-lab/host-ro/source.txt
printf 'before
' > da21-lab/host-rw/state.txt
pwd
ls -ln da21-lab/host-ro da21-lab/host-rw
docker context inspect "$(docker context show)" --format '{{json .Endpoints.docker.Host}}' | tee da21-lab/evidence/context-endpoint.txt
The source directories are synthetic and intentionally narrow. If
the active context points to a remote daemon, stop: the local paths
shown by pwd are client paths and may not exist on that
daemon. The mandatory writable-bind steps assume a local Engine or
Docker Desktop context.
2. Read-only bind: prove visibility without mutation
The container may read the file but must not modify it.
--mount makes the source/target/type explicit.
docker run --rm --name da21-ro --label devops-academy.lab=ch21 --mount type=bind,src="$(pwd)/da21-lab/host-ro",dst=/input,readonly busybox:1.36.1 sh -c '
set -eu
cat /input/source.txt
if echo blocked > /input/should-not-write.txt 2>/tmp/err; then
echo "unexpected write success" >&2; exit 1
else
echo "write blocked as expected"
cat /tmp/err
fi
' | tee da21-lab/evidence/readonly.txt
ls -la da21-lab/host-ro
No new host file should exist. A read-only bind constrains writes at the mount boundary rather than relying only on application convention.
3. Writable bind: prove host-visible side effects
Now grant write authority only to the disposable
host-rw directory.
docker run --rm --name da21-rw --label devops-academy.lab=ch21 --mount type=bind,src="$(pwd)/da21-lab/host-rw",dst=/work busybox:1.36.1 sh -c '
set -eu
printf "changed-by-container
" >> /work/state.txt
printf "created-by-container
" > /work/container-created.txt
ls -ln /work
' | tee da21-lab/evidence/writable-container.txt
cat da21-lab/host-rw/state.txt
cat da21-lab/host-rw/container-created.txt
ls -ln da21-lab/host-rw | tee da21-lab/evidence/writable-host.txt
The container has modified client-visible host files because this lab uses a local/Desktop context. Numeric ownership may differ by platform and Desktop file-sharing implementation, so record rather than assume it.
4. Inspect mount state before the container exits
For stronger evidence, create a sleeping container, inspect its mount object, then remove the exact container.
docker create \
--name da21-inspect \
--label devops-academy.lab=ch21 \
--mount type=bind,src="$(pwd)/da21-lab/host-rw",dst=/work,readonly busybox:1.36.1 sleep 300
docker inspect da21-inspect --format '{{json .Mounts}}' | tee da21-lab/evidence/mount-inspect.json
docker rm da21-inspect
Look for type, source, destination, RW:false, and
propagation. The source recorded by the daemon is the authoritative
host-side path it resolved.
5. Strict source handling versus silent creation
This intentionally broken example preserves the first error. The
first command should fail because --mount requires the
source path to exist by default.
rm -rf da21-lab/missing-strict da21-lab/missing-short
if docker run --rm --mount type=bind,src="$(pwd)/da21-lab/missing-strict",dst=/data busybox:1.36.1 true >da21-lab/evidence/strict.stdout 2>da21-lab/evidence/strict.stderr; then
echo "unexpected success"
else
echo "strict mount failed as expected"
cat da21-lab/evidence/strict.stderr
fi
test ! -e da21-lab/missing-strict && echo 'strict source was not created'
# Demonstrate the historical -v behavior only inside the disposable lab directory.
docker run --rm -v "$(pwd)/da21-lab/missing-short:/data" busybox:1.36.1 sh -c 'test -d /data && echo mounted'
test -d da21-lab/missing-short && echo 'short syntax created the missing directory'
The lesson is not “always avoid -v.” The lesson is to
know whether source creation is intended. Current Engine also offers
explicit bind-create-src with --mount; use
that only when your installed Engine supports it and creation is
part of the design.
6. tmpfs: bounded ephemeral state
Create a container with a 1 MiB tmpfs and a 64 MiB container memory limit. The tmpfs has no host source path.
docker run -d \
--name da21-tmpfs \
--label devops-academy.lab=ch21 \
--memory 64m \
--mount type=tmpfs,dst=/run/lab,tmpfs-size=1048576,tmpfs-mode=1770 busybox:1.36.1 sh -c 'printf "ephemeral
" > /run/lab/token.txt; sleep 300'
docker inspect da21-tmpfs --format '{{json .Mounts}}' | tee da21-lab/evidence/tmpfs-inspect.json
docker exec da21-tmpfs sh -c 'cat /run/lab/token.txt; df -h /run/lab; ls -ldn /run/lab'
docker rm -f da21-tmpfs
docker run \
--rm \
--mount type=tmpfs,dst=/run/lab,tmpfs-size=1048576,tmpfs-mode=1770 busybox:1.36.1 sh -c 'test ! -e /run/lab/token.txt && echo "previous tmpfs data is gone"'
7. Subpath is mount-type-specific
Current Docker documentation explicitly supports
volume-subpath for named volumes. This is useful to
teach the concept without inventing a bind-mount option: for a bind
mount, the normal way to expose only a subtree is to bind the
narrower host path itself.
docker volume create --label devops-academy.lab=ch21 da21-subpath
docker run \
--rm \
--mount type=volume,src=da21-subpath,dst=/seed busybox:1.36.1 sh -c 'mkdir -p /seed/team-a /seed/team-b; echo A > /seed/team-a/value; echo B > /seed/team-b/value'
docker run \
--rm \
--mount type=volume,src=da21-subpath,dst=/data,volume-subpath=team-a,readonly busybox:1.36.1 sh -c 'cat /data/value; test ! -e /data/../team-b/value || true'
docker volume rm da21-subpath
If the installed Engine predates volume-subpath support, record that as a compatibility limitation and skip this optional comparison. The mandatory bind-mount lessons do not depend on it.
8. SELinux and propagation: inspect and reason, do not weaken the host
On an SELinux host, check getenforce if available. If
access is denied, inspect labels and use the documented
z/Z semantics only for a deliberately
scoped path; do not disable SELinux. Bind propagation defaults to
rprivate, is Linux-host-specific, requires the host
mount setup to support propagation, and does not work on Docker
Desktop. For this cross-platform lab, propagation remains a
conceptual/inspection exercise rather than a host-mount mutation.
command -v getenforce >/dev/null && getenforce || true
docker run -d --name da21-prop-inspect --mount type=bind,src="$(pwd)/da21-lab/host-ro",dst=/data,readonly busybox:1.36.1 sleep 300
docker inspect da21-prop-inspect --format '{{range .Mounts}}{{.Type}} {{.Destination}} RW={{.RW}} Propagation={{.Propagation}}{{println}}{{end}}'
docker rm -f da21-prop-inspect
9. Challenge: choose the correct layer
Your laptop file exists, but a container launched through a remote Docker context reports “bind source path does not exist.” Which layer is wrong? The container image is not the first suspect. Prove the active context and daemon endpoint, then ask whether the source exists on that daemon host.
10. Exact cleanup
docker rm -f da21-ro da21-rw da21-inspect da21-tmpfs da21-prop-inspect 2>/dev/null || true
docker volume rm da21-subpath 2>/dev/null || true
# Review evidence before deleting the disposable host paths.
find da21-lab -maxdepth 2 -type f -print
# Delete only ./da21-lab when you intentionally finish the lab.
Knowledge check
Why did the strict --mount example fail?
Its source path did not exist, and bind mounts using
--mount require an existing source by default.
What risk does -v demonstrate when the source path
is misspelled?
It can create a directory silently, masking the typo and mounting an empty path instead of the intended data.
Why is tmpfs suitable for disposable runtime material?
Its contents are memory-backed and disappear when the container stops, so they do not become persistent host or image state.
Why is the subpath example a named-volume example rather than a bind-mount claim?
Current Docker documentation explicitly defines
volume-subpath; bind mounts normally expose a
narrower host path directly.
What should you do instead of disabling SELinux after a mount denial?
Preserve the denial, inspect labels/policy, and apply the correct scoped labeling semantics only when appropriate.
Official references and version notes
-
Docker Docs — Bind mounts
— daemon-host paths, read-only mode,
--mountversus-v, recursive mounts, propagation, and SELinux labeling. - Docker Docs — tmpfs mounts — memory-backed lifetime, size/mode options, and cgroup memory accounting.
- Docker CLI — docker container run — mount and read-only-root-filesystem semantics.
-
Compose Specification — service volumes
— bind options,
create_host_path, SELinux labels, tmpfs, and volume subpaths. -
Docker Docs — Volumes
—
volume-subpathas a mount-type-specific subpath feature and contrast with bind mounts. - Docker Desktop — File sharing — host-to-VM sharing boundaries and performance guidance.
- Docker Desktop — Synchronized file shares — optional paid synchronized host-file caching for large repositories.
-
Docker Engine 29 release notes
— current Engine baseline and
bind-create-srcaddition in 29.3.
Docker Engine 29.8.1 and Docker Desktop 4.91.0 are current; Compose 5.5.1, Buildx 0.37.1, and BuildKit 0.33.0 are the upstream baselines used for compatibility discussion. The labs record the learner's actual versions because host kernel, Docker Desktop VM/file-sharing mode, SELinux, and remote-context topology materially affect mount behavior.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.