Docker & ContainersXVI Β· PerformanceStorage drivers
Storage drivers and image layout
What you'll learn
- Explain the on-disk layout overlay2 builds under /var/lib/docker/overlay2
- Measure copy-up cost and predict which workloads pay it
- Verify the backing filesystem supports d_type and know what happens when it does not
- State which storage drivers still exist in current Docker Engine and which were removed
- Plan a driver change, including what happens to existing images
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
The storage driver is the implementation behind every image pull, every
container start, and every byte a container writes to its own filesystem.
overlay2 is the default on Linux and has been for years. Knowing what it
does well, what it does badly, and which of your problems it is not
responsible for is the difference between debugging a performance issue and
guessing at one.
What is still here, and what is gone
This table is more useful than it looks, because half the storage-driver advice on the internet recommends drivers that no longer exist in the engine.
| Driver | Status in current Engine |
|---|---|
overlay2 | Supported. The classic default on Linux. |
| containerd snapshotters | Default for Engine 29.0 and later on fresh installs. |
fuse-overlayfs | Supported, for rootless Docker on hosts where overlay2 cannot run rootless. |
btrfs | Supported on a Btrfs backing filesystem. |
zfs | Supported on a ZFS backing filesystem. |
vfs | Supported, for testing. No copy-on-write; every layer is a full copy. |
aufs | Removed in v24.0. Deprecated in v19.03. |
overlay (v1) | Removed in v24.0. Deprecated in v18.09. |
devicemapper | Removed in v25.0. Deprecated in v18.09, disabled by default in v23.0. |
Three practical consequences:
βFall back to devicemapperβ is no longer advice. It is not in the engine.
A host that cannot run overlay2 runs vfs, which has no copy-on-write at
all β every container gets a full physical copy of the entire image β or it
does not run current Docker.
An upgrade across a removal boundary is a data event. Upgrading a host
that was still on devicemapper to Engine 25.0 leaves the daemon unable to
read any of its images. The images are still on disk; nothing can open them.
Check docker info before an engine upgrade, not after.
Engine 29 changed the default. New installations use the containerd
snapshotter rather than the classic overlay2 graph driver. If a lesson,
runbook or script of yours reasons about /var/lib/docker/overlay2 paths, it
is reasoning about the classic layout, which is still what you get on an
upgraded 28.x host and on 29.x hosts that were upgraded rather than
reinstalled.
The on-disk layout
docker info --format 'driver={{.Driver}}'
docker info | sed -n '/Storage Driver/,/^ [A-Z]/p'$ docker infoStorage Driver: overlay2
Backing Filesystem: xfs
Supports d_type: true
Using metacopy: false
Native Overlay Diff: true
userxattr: falseIllustrative output
Each layer is a directory under /var/lib/docker/overlay2/:
/var/lib/docker/overlay2/
βββ l/ # short symlinks to every diff dir
β βββ ZLNGY6... -> ../<layer-id>/diff
βββ <layer-id>/
βββ diff/ # this layer's actual files
βββ link # this layer's short name, as used in l/
βββ lower # colon-separated list of parent layers
βββ merged/ # the unified view, mounted only while running
βββ work/ # OverlayFS scratch space for copy-up
The l/ directory of two-character-ish symlinks looks like an odd design
choice until you know why it exists: the mount syscall takes its options as
a single page of text, and an image with forty layers produces a lowerdir=
argument of forty 64-character paths, which overflows it. The short symlinks
keep the mount options inside the limit.
CONTAINER=web
docker inspect --format '{{.GraphDriver.Name}}' "$CONTAINER"
docker inspect --format '{{.GraphDriver.Data.UpperDir}}' "$CONTAINER"
# The same thing as the kernel sees it.
MERGED=$(docker inspect --format '{{.GraphDriver.Data.MergedDir}}' "$CONTAINER")
findmnt -no SOURCE,FSTYPE,OPTIONS "$MERGED" | tr ',' '\n' | head -20GraphDriver.Data.UpperDir is the single most useful path in container
debugging. It is the containerβs writable layer, on the host, readable with
ordinary tools while the container runs. Everything the container has created
or modified since it started is in there, and du -sh on it answers βhow much
has this container written into its own filesystemβ directly.
What copy-up costs
Reads of unmodified files are nearly free: OverlayFS looks up the path in
upper, misses, and falls through to the lower layers. One extra directory
lookup, then a normal read from the backing filesystem.
The first write to a file that lives in a lower layer triggers a
copy-up: the entire file is copied from the lower layer to upper before
the write is applied. Not the modified block β the whole file.
| Operation | Cost |
|---|---|
| Read a file never modified | Near-native |
| First write to a 4 KB config file | Copy 4 KB, then write. Negligible. |
| First write to a 2 GB database file in the image | Copy 2 GB, then write 8 KB |
| Subsequent writes to the same file | Near-native; it is already in upper |
| Delete a file from a lower layer | Whiteout entry in upper. Cheap. |
| Create a new file | Straight into upper. Native speed. |
The third row is the one that ends up in an incident report. A container appends one record to a large file that shipped inside the image, and the first append takes as long as copying the whole file.
cat > /tmp/copyup.Dockerfile <<'EOF'
FROM alpine:3.20
RUN dd if=/dev/zero of=/big.dat bs=1M count=512 2>/dev/null
EOF
docker build -q -t copyup-demo -f /tmp/copyup.Dockerfile /tmp
docker run --rm copyup-demo sh -c '
echo "--- first write (triggers copy-up of 512 MB) ---"
time dd if=/dev/zero of=/big.dat bs=1M count=1 conv=notrunc 2>/dev/null
echo "--- second write (already in upper) ---"
time dd if=/dev/zero of=/big.dat bs=1M count=1 conv=notrunc 2>/dev/null
'$ docker run --rm copyup-demo sh -c '...'--- first write (triggers copy-up of 512 MB) ---
real 0m 2.41s
user 0m 0.00s
sys 0m 0.71s
--- second write (already in upper) ---
real 0m 0.01s
user 0m 0.00s
sys 0m 0.00sIllustrative output
Two hundred times slower for an identical operation. That ratio is the whole argument for volumes.
The backing filesystem requirement
overlay2 needs the underlying filesystem to distinguish file types in
directory entries β d_type support. On ext4 this is always present. On
XFS it depends on how the filesystem was formatted, and this is the trap.
XFS created with ftype=0 β the default for mkfs.xfs on some older
distributions, and the layout on plenty of long-lived hosts β does not provide
d_type. overlay2 on such a filesystem does not refuse to start. It runs, and
misbehaves: whiteouts do not work correctly, so deleted files can reappear,
and layer extraction can produce a filesystem that does not match the image.
docker info | grep -E 'Backing Filesystem|Supports d_type'
# If the backing filesystem is xfs, confirm at the filesystem level too.
DEV=$(findmnt -no SOURCE --target /var/lib/docker)
sudo xfs_info "$DEV" 2>/dev/null | grep -o 'ftype=[01]' \
|| echo 'not xfs, or xfs_info unavailable'ftype=1 and Supports d_type: true is the only acceptable result. ftype=0
cannot be changed in place: XFS cannot be reformatted online and there is no
xfs_admin flag for it. The fix is to back up, recreate the filesystem with
mkfs.xfs -n ftype=1, and restore β which is why you check this when you
build the host, not when you are debugging phantom files six months later.
Choosing, and changing
For essentially every host: leave it alone.
- ext4 or XFS with
ftype=1, kernel 4.0+:overlay2. No reason to change. - Btrfs or ZFS root filesystem: the matching driver, if you want the
filesystemβs own snapshot tooling to see container layers.
overlay2on top of them also works and is simpler. - Rootless Docker where
overlay2will not run:fuse-overlayfs. - Fresh Engine 29 install: you already have the containerd snapshotter and should keep it.
- Anything else: you are on
vfs, which is a correctness fallback rather than a production choice, and the real fix is the host.
OUT=/var/tmp/pre-driver-change-$(date +%F)
mkdir -p "$OUT"
docker image ls --digests --format '{{.Repository}}:{{.Tag}} {{.Digest}}' > "$OUT/images.txt"
docker ps -a --format '{{.Names}} {{.Image}} {{.Status}}' > "$OUT/containers.txt"
docker volume ls --format '{{.Name}}' > "$OUT/volumes.txt"
docker info --format '{{.Driver}}' > "$OUT/driver.txt"
echo "copy $OUT off this host before proceeding"Verification that can fail
After any change, or as a periodic health check:
FAIL=0
DRIVER=$(docker info --format '{{.Driver}}')
echo "driver: $DRIVER"
case "$DRIVER" in
vfs) echo 'FAIL: vfs has no copy-on-write; every container is a full image copy'; FAIL=1 ;;
esac
if docker info 2>/dev/null | grep -q 'Supports d_type: false'; then
echo 'FAIL: backing filesystem lacks d_type; overlay2 behaviour is undefined'
FAIL=1
fi
# Any warning the daemon emits about storage is worth failing on.
docker info 2>&1 | sed -n '/^WARNING/p'
exit "$FAIL"And to check the thing that actually causes storage-driver performance complaints β workloads writing into the writable layer instead of a volume:
$ docker ps -s --format 'table {{.Names}}\t{{.Size}}'NAMES SIZE
nginx 1.09kB (virtual 48.3MB)
api 32.8kB (virtual 214MB)
worker 4.1kB (virtual 214MB)
legacy-db 8.4GB (virtual 8.72GB)Illustrative output
legacy-db is the finding. Eight gigabytes in a writable layer means every
one of those bytes is on the Docker partition, unbacked-up, deleted with the
container, and paying copy-up on any file that came from the image. That is
not a storage driver problem, and switching drivers will not help it.
Knowledge check
Knowledge check Β· 5 questions
Q1. A container appends 8 KB to a 2 GB file that shipped inside its image. The first append is slow, subsequent ones are fast. Why?
Q2. Your host runs Docker Engine 25.0 and `docker info` reports `Storage Driver: vfs`. What does that tell you?
Q3. `docker info` shows `Backing Filesystem: xfs` and `Supports d_type: false`. What is the correct response?
Q4. Which of these storage drivers have been removed from Docker Engine? Select all that apply.
Q5. Fifty containers started from the same base image share a single page cache entry per file in the shared lower layers.
Passing score: 75%. Answers are checked in this browser.