Docker & ContainersVIII Β· StorageNetwork storage
Network and shared storage β NFS, CIFS, Ceph, iSCSI
What you'll learn
- Create NFS and CIFS volumes with the local driver and the correct mount options
- Explain how block storage (iSCSI, Ceph RBD) reaches a container, and why it is not a volume option
- Predict what a container does when the storage server disappears
- Recognise why shared file storage is unsafe for a single-writer database file
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
Local volume storage on a single host cannot survive host failure. For stateful workloads that need to survive, the volume must live on shared storage. The standard choices are NFS, CIFS, Ceph, and iSCSI.
Before any of them: how Docker actually gets there.
It is mount(8), not a volume driver
NFS_SERVER=192.0.2.10
NFS_EXPORT=/srv/exports/appdata
mkdir -p /mnt/nfs-test
mount -t nfs -o "addr=$NFS_SERVER,rw,nfsvers=4.2" "$NFS_SERVER:$NFS_EXPORT" /mnt/nfs-test
findmnt -no FSTYPE,OPTIONS /mnt/nfs-test
umount /mnt/nfs-testNFS
NFS_SERVER=192.0.2.10
NFS_EXPORT=/srv/exports/appdata
docker volume create --driver local --opt type=nfs --opt o="addr=$NFS_SERVER,rw,nfsvers=4.2,hard,timeo=600,retrans=2" --opt device=":$NFS_EXPORT" appdata-nfsNote the leading colon on device. The mount -t nfs source is
host:/path, and because the address is already in o=addr=, the
host part is left empty β :/srv/exports/appdata. Omitting that
colon is the most common reason a freshly created NFS volume fails
on first use.
The mount options that decide how failures behave
| Option | Effect | When you want it |
|---|---|---|
hard (default) | Requests retry indefinitely | Almost always |
soft | Requests fail after retrans retries | Almost never β see below |
nfsvers=4.2 | Pin the protocol version | Always, so a server change cannot silently downgrade you |
timeo=600 | Retransmit timeout in deciseconds (60 s) | With hard, to slow the retry storm |
nolock | Disable NLM locking | Only when you have read the warning below |
noac | Disable attribute caching | Rarely; large correctness-for-throughput trade |
ro | Read-only | Whenever the workload does not write |
If nfsvers is unspecified the client tries 4.2 and negotiates
down until the server agrees. That is convenient and it means a
server maintenance window can leave you on NFSv3 without anyone
noticing, which changes the locking model underneath a running
workload. Pin it.
CIFS / SMB
For Windows file servers, or Linux hosts sharing storage with Windows.
SMB_HOST=198.51.100.20
SMB_SHARE=appdata
docker volume create --driver local --opt type=cifs --opt o="addr=$SMB_HOST,credentials=/etc/docker-smb.creds,vers=3.1.1,uid=1001,gid=1001,file_mode=0640,dir_mode=0750" --opt device="//$SMB_HOST/$SMB_SHARE" appdata-cifsCIFS differs from NFS in a way that matters here: the server does
not carry Unix ownership, so uid, gid, file_mode and
dir_mode are client-side fictions applied to everything on the
mount. Every file appears owned by whatever uid= you passed. This
neatly sidesteps the UID mismatch problem from the permissions
lesson β and it means the permission bits you see are not enforcing
anything on the server, which is a different problem if you were
relying on them.
Put the credentials in a root-owned 0600 file, never in o=:
username=svc-appdata
password=REPLACE_ME
domain=EXAMPLE
Ceph and iSCSI: block storage reaches a container differently
Neither of these is a volume option, and treating them as one is the error the shape of the CLI invites.
Ceph RBD. The host maps the image to a block device, you put a filesystem on it once, and the container gets a filesystem:
POOL=containers
IMG=appdata
# Host side: map the RBD image to a block device
rbd map "$POOL/$IMG" --name client.docker
DEV=$(rbd showmapped --format json | python3 -c 'import json,sys;print([m["device"] for m in json.load(sys.stdin).values()][0])')
# ONCE, at provisioning time only:
# mkfs.ext4 "$DEV"
mkdir -p /mnt/appdata
mount "$DEV" /mnt/appdataThe container then bind-mounts /mnt/appdata, or you wrap the
already-mounted path in a local volume with type=none and
o=bind.
iSCSI. Same shape, one layer lower: iscsiadm discovers and
logs in to the target, a /dev/sd* appears, multipath aggregates
the paths, and you mount a filesystem on it.
The operational point for both: the block device is exclusive.
An RBD image or an iSCSI LUN with a non-clustered filesystem
(ext4, xfs) mounted read-write on two hosts at once is not a
sharing model, it is immediate corruption β worse than the NFS case,
because there is no network filesystem layer even attempting
coherence. Clustered filesystems (GFS2, OCFS2) exist for exactly
this and bring a cluster manager with them.
CephFS is the file-level Ceph interface, and because the host kernel can mount it, the local driver can too:
docker volume create --driver local \
--opt type=ceph \
--opt o=name=docker,secretfile=/etc/ceph/docker.secret \
--opt device=192.0.2.30:6789:/volumes/appdata \
appdata-cephfs
Docker does not document type=ceph; it works because
mount -t ceph works on that host. Verify it by hand first, as
above, and treat the kernel client version as part of your
compatibility matrix.
Choosing
| Storage | Sharing model | Good for | Bad for |
|---|---|---|---|
| Local NVMe / SSD | None | Databases, anything latency-sensitive | Surviving host loss |
| NFSv4.2 | Many readers and writers, real locking | Uploads, exports, shared config, media | Single-writer file databases |
| CIFS/SMB 3.x | Many readers and writers, oplocks | Windows interop, document shares | POSIX semantics, high IOPS |
| CephFS | Many readers and writers, POSIX | Shared files with no single server | Small-cluster simplicity |
| Ceph RBD | One host at a time | Databases needing HA and failover | Concurrent multi-host mounts |
| iSCSI | One host at a time (without a clustered FS) | Enterprise SAN integration | Concurrent multi-host mounts |
The column that matters is the middle one, not throughput. Pick the sharing model your workloadβs consistency requirements need, then worry about performance.
Verification that can fail
VOL=appdata-nfs
# 1. Does it mount at all? (Volume creation does not test this.)
docker run --rm -v "$VOL":/data:ro alpine:3.20 ls /data
# 2. Are the options the ones you set? findmnt reads the kernel, not your intent.
findmnt -t nfs,nfs4,cifs,ceph -no TARGET,SOURCE,OPTIONS | grep -F "$(docker volume inspect "$VOL" --format '{{.Mountpoint}}')"
# 3. Is it actually writable end to end?
docker run --rm -v "$VOL":/data alpine:3.20 sh -c 'dd if=/dev/zero of=/data/.iotest bs=1M count=8 conv=fsync && rm /data/.iotest'$ findmnt -t nfs4 -no TARGET,SOURCE,OPTIONS/var/lib/docker/volumes/appdata-nfs/_data 192.0.2.10:/srv/exports/appdata rw,relatime,vers=4.2,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,local_lock=none,addr=192.0.2.10Illustrative output
Step 2 is the one people skip and the one that catches the real
errors: an o= string with a typo in an option name is silently
ignored by some filesystem types, so a mount you believe is hard
and nfsvers=4.2 can be neither. findmnt shows what the kernel
recorded, which is the only version that counts.
In that output, vers=4.2 and hard are the two tokens to look
for. If vers=3 appears, the client negotiated down and the locking
model is not the one you designed against. If soft appears, or
local_lock is anything other than none, someone pasted an
option set without reading it.
Step 3 uses conv=fsync deliberately. Without it the write lands in
the page cache and returns success even when the server is
unreachable.
Knowledge check
Knowledge check Β· 5 questions
Q1. What does `--opt type=nfs` on a `local` volume actually do?
Q2. Why is the NFS `soft` option dangerous compared with `hard`?
Q3. Which of these are genuinely NOT Docker volume drivers or `type=` values? Select all that apply.
Q4. With `nolock`, file locks on an NFS mount fail with an error, so an application will notice it cannot lock.
Q5. A container writing to a `hard`-mounted NFS volume stops responding after the NFS server fails. `docker stop` and `docker kill` both do nothing. Why?
Passing score: 75%. Answers are checked in this browser.