Skip to main content
RunBook Academy

Docker & ContainersVIII Β· StorageNetwork storage

Network and shared storage β€” NFS, CIFS, Ceph, iSCSI

Advanced⏱ ~26 min

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

Not yet marked complete on this device.

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

Read-only / Safeprove it by hand first
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-test

NFS

Read-only / Safenfs volume
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-nfs

Note 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

OptionEffectWhen you want it
hard (default)Requests retry indefinitelyAlmost always
softRequests fail after retrans retriesAlmost never β€” see below
nfsvers=4.2Pin the protocol versionAlways, so a server change cannot silently downgrade you
timeo=600Retransmit timeout in deciseconds (60 s)With hard, to slow the retry storm
nolockDisable NLM lockingOnly when you have read the warning below
noacDisable attribute cachingRarely; large correctness-for-throughput trade
roRead-onlyWhenever 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.

Read-only / Safecifs volume
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-cifs

CIFS 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:

Destructiverbd map
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/appdata

The 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

StorageSharing modelGood forBad for
Local NVMe / SSDNoneDatabases, anything latency-sensitiveSurviving host loss
NFSv4.2Many readers and writers, real lockingUploads, exports, shared config, mediaSingle-writer file databases
CIFS/SMB 3.xMany readers and writers, oplocksWindows interop, document sharesPOSIX semantics, high IOPS
CephFSMany readers and writers, POSIXShared files with no single serverSmall-cluster simplicity
Ceph RBDOne host at a timeDatabases needing HA and failoverConcurrent multi-host mounts
iSCSIOne host at a time (without a clustered FS)Enterprise SAN integrationConcurrent 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

Read-only / Safeverify a network volume
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'
Read-only / Safefindmnt on a working NFS volume
$ 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.10

Illustrative 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

  1. Q1. What does `--opt type=nfs` on a `local` volume actually do?

  2. Q2. Why is the NFS `soft` option dangerous compared with `hard`?

  3. Q3. Which of these are genuinely NOT Docker volume drivers or `type=` values? Select all that apply.

  4. Q4. With `nolock`, file locks on an NFS mount fail with an error, so an application will notice it cannot lock.

  5. 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.