| |

Docker 30 🐳 Storage Drivers Deep Dive: Overlay2 Architecture, Inodes, and Performance Benchmarks

The storage driver is the layer between a container’s filesystem and the host’s disk. It determines how image layers are stacked, how writes are handled, and how much space and inode overhead the container incurs. Docker supports several drivers, but overlay2 is the default on most Linux distributions and the one most LFCS and Docker exam candidates will encounter. Understanding its architecture, its inode behavior, and its performance characteristics is necessary for diagnosing storage problems and making informed driver choices.

Key point: overlay2 is a union filesystem that stacks read-only image layers (lowerdir) beneath a writable container layer (upperdir), presenting a unified view through a merged directory. Writes trigger a copy-up of the entire file from the lower layer to the upper layer. The driver is file-level copy-on-write, which means large-file modifications are slow but container startup is fast. Inode exhaustion and layer depth are the two most common failure modes.


Why overlay2 is the default

The union mount problem. A Docker image is a stack of layers. Each layer is a diff—a set of files that were added or changed in that step. When a container runs, it needs to see a single, coherent filesystem that merges all the image layers plus its own writable layer. OverlayFS provides this union mount. The overlay2 driver is Docker’s implementation of that union mount, and it is more efficient than the legacy overlay driver in inode utilization and layer count support .

The inode problem. The legacy overlay driver consumed excessive inodes, especially as the number of images grew. overlay2 takes advantage of kernel features added in Linux 4.0 to avoid this excessive consumption . It supports up to 128 lower layers natively, which allows multi-layer images to be stacked directly rather than through hard links .

The compatibility problem. overlay2 requires a backing filesystem that supports d_type=true. On XFS, this means the filesystem must be formatted with ftype=1. On ext4, d_type is enabled by default in modern versions. Without d_type, overlay2 degrades to a slower mode and may fail .

The performance problem. overlay2 provides the fastest container startup among the storage drivers, averaging around 50ms, compared to 80–90ms for btrfs and zfs . It also has good sequential write and random I/O performance. Its main weakness is copy-on-write of large files, which copies the entire file rather than just the changed blocks .


a. Overlay2 architecture: lowerdir, upperdir, merged

The overlay2 driver constructs a container’s filesystem from three directories and one file, all managed by Docker under /var/lib/docker/overlay2/. You should never manipulate these directly .

ComponentRole
lowerdirRead-only image layers, stacked in order
upperdirWritable container layer
mergedUnified view, the container’s mount point
workdirInternal to OverlayFS, for atomic operations
lower-idFile containing the ID of the top image layer

The lowerdir is the image. It is read-only. When a container is created, Docker combines the image’s top layer with a new writable directory for the container. The image layers become the lowerdir; the new directory becomes the upperdir .

The merged directory is the union of the two. From inside the container, / is the merged directory. The container sees the files from the image layers and its own writable layer as a single filesystem.

The workdir is required by OverlayFS for internal operations. It must be on the same filesystem as the upperdir .

When a file exists in both the image layer and the container layer, the container layer’s version wins. The image layer’s file is obscured. This is how modifications are represented: the modified file is copied to the upper layer, and the lower layer’s version is hidden .


b. Copy-up, whiteouts, and inode behavior

Copy-up. The first time a container writes to a file that exists in the image layer, overlay2 performs a copy-up. The entire file is copied from the lowerdir to the upperdir. The container’s write then modifies the copy in the upper layer. The lower layer’s original file is unchanged .

This is file-level copy-on-write. If the file is 1 GB and the container modifies one byte, the entire 1 GB is copied. This is the primary performance weakness of overlay2 compared to btrfs or zfs, which copy only the changed blocks .

Subsequent writes to the same file operate on the copy in the upperdir. The copy-up happens only once per file .

Whiteouts. When a container deletes a file that exists in the image layer, the file cannot be removed from the read-only lowerdir. Instead, overlay2 creates a whiteout file in the upperdir that obscures the lower layer’s file. The file is effectively deleted from the container’s view, but the image layer’s copy still exists .

Inode consumption. Inodes are consumed by the files and directories in the upperdir. A container that creates many small files—a build process, a cache, a log directory—consumes inodes in proportion to the number of files. The overlay2 driver itself has a fixed inode footprint per layer, and the legacy overlay driver’s excessive inode consumption is what overlay2 was designed to fix .

Inode exhaustion. The most common storage failure in Docker is inode exhaustion, not disk space exhaustion. The error message is No space left on device, but df -h shows free space. The fix is df -i, which shows inode usage. If the inode count is near 100%, the filesystem cannot create new files, even though there is space .

The mitigation is to monitor df -i /var/lib/docker and clean up unused images, containers, and volumes with docker system prune -a --volumes . For long-term prevention, the backing filesystem can be formatted with a higher inode density: mkfs.ext4 -i 131072 allocates one inode per 128 KB, which is more dense than the default .


c. Performance benchmarks and comparison

Container start time. overlay2 is the fastest. A typical container starts in around 50ms, compared to 80ms for btrfs and 90ms for zfs .

Sequential write. All three drivers perform well for sequential writes. overlay2 is good, btrfs is good, and zfs is good with compression enabled .

Random I/O. overlay2 and zfs perform well. btrfs is moderate. This matters for databases and applications that do many small reads and writes .

Copy-on-write of large files. This is where overlay2 is weakest. Because it copies the entire file, modifying a large file is slow. btrfs and zfs copy only changed blocks, making them significantly faster for this operation .

Storage efficiency. overlay2 is good. btrfs and zfs are excellent when compression is enabled, but compression is optional and must be configured .

The performance trade-off. overlay2 is the default for a reason: it provides the best balance of startup speed, I/O performance, and compatibility for most workloads. It is not the best for write-heavy workloads that modify large files, but those workloads are usually better served by volumes than by the container’s writable layer anyway.


Complete Example Session

# ============================================
# PART 1: CHECK THE CURRENT DRIVER
# ============================================
docker info | grep -E "(Storage Driver|Backing Filesystem|Supports d_type)"
# ============================================
# PART 2: VIEW OVERLAY MOUNTS
# ============================================
mount | grep overlay
# Shows the lowerdir, upperdir, and workdir for each running container
# ============================================
# PART 3: INSPECT A CONTAINER'S LAYER DIRECTORIES
# ============================================
ls -l /var/lib/docker/overlay2/
# Shows the layer directories
ls -l /var/lib/docker/overlay2/<container-id>/
# Shows lower, upper, merged, work
# ============================================
# PART 4: VERIFY THE MERGED VIEW
# ============================================
ls -l /var/lib/docker/overlay2/<container-id>/merged/
# Shows the container's filesystem
# ============================================
# PART 5: CHECK INODE USAGE
# ============================================
df -i /var/lib/docker
# If near 100%, inode exhaustion
# ============================================
# PART 6: CHECK DISK SPACE
# ============================================
df -h /var/lib/docker
# Space used by overlay2
# ============================================
# PART 7: CLEAN UP UNUSED RESOURCES
# ============================================
docker system prune -a --volumes
# Removes unused images, containers, volumes, networks
# ============================================
# PART 8: CHECK FILESYSTEM TYPE
# ============================================
xfs_info /var/lib/docker | grep ftype
# or
tune2fs -l /dev/sdX | grep "Filesystem features"
# ============================================
# PART 9: CONFIGURE OVERLAY2 IN DAEMON.JSON
# ============================================
cat > /etc/docker/daemon.json << EOF
{
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.mountopt=nodev,metacopy=on"
  ]
}
EOF
systemctl restart docker
# ============================================
# PART 10: BENCHMARK I/O WITH DD
# ============================================
docker run -it --rm ubuntu bash -c "dd if=/dev/zero of=test.img bs=1G count=1 oflag=dsync"

The ten parts covered checking the driver, viewing overlay mounts, inspecting layer directories, verifying the merged view, checking inode usage, checking disk space, cleaning up, checking the filesystem type, configuring overlay2, and benchmarking I/O.


Quick Reference

Overlay2 Components

ComponentPathRole
lowerdir/var/lib/docker/overlay2/<id>/diffRead-only image layers
upperdir/var/lib/docker/overlay2/<id>/diffWritable container layer
merged/var/lib/docker/overlay2/<id>/mergedUnified view
workdir/var/lib/docker/overlay2/<id>/workInternal
lower-id/var/lib/docker/overlay2/<id>/lower-idTop image layer ID

Overlay2 vs Other Drivers

Metricoverlay2btrfszfs
Container start~50ms~80ms~90ms
Sequential writeGoodGoodGood
Random I/OGoodModerateGood
CoW (large files)Slow (file-level)Fast (block-level)Fast (block-level)
Storage efficiencyGoodExcellent (compression)Excellent (compression)
Best forGeneral purposeSnapshot-heavyData integrity

Requirements

RequirementDetail
Kernel4.0+ (or RHEL 3.10.0-514+)
ext4d_type enabled (default in modern)
XFSftype=1 (format with -n ftype=1)
Backing FSext4 or XFS (not btrfs/zfs)

Inode Management

TaskCommand
Check inode usagedf -i /var/lib/docker
Clean unuseddocker system prune -a --volumes
High-density formatmkfs.ext4 -i 131072 /dev/sdX
Monitordf -i above 90% needs action

Best Practices

✅ Do This:

# Verify the backing filesystem supports d_type
docker info | grep "Supports d_type"                         # ✅

# Monitor inode usage, not just disk space
df -i /var/lib/docker                                        # ✅

# Clean up unused resources regularly
docker system prune -a --volumes                             # ✅

# Use volumes for write-heavy data
docker run -v mydata:/var/lib/postgresql/data postgres        # ✅

# Reduce image layers in the Dockerfile
# Use multi-stage builds, combine RUN commands                 # ✅

# Enable metacopy for faster copy-up (kernel 4.19+)
"storage-opts": ["overlay2.mountopt=metacopy=on"]             # ✅

# Use XFS with ftype=1 for the backing filesystem
mkfs.xfs -n ftype=1 /dev/sdX                                 # ✅

❌ Don’t Do This:

# Don't manually manipulate overlay2 directories
rm -rf /var/lib/docker/overlay2/*                            # ❌

# Don't assume "No space left on device" means disk full
# Check inodes first                                            # ❌

# Don't use overlay2 on btrfs or zfs
# Unsupported and will fail                                     # ❌

# Don't ignore d_type requirements
# overlay2 degrades without it                                  # ❌

# Don't store databases in the container writable layer
# Use volumes for persistence                                   # ❌

# Don't let layer count grow without reason
# Deep stacks slow down file lookups                            # ❌

# Don't prune without checking what will be removed
docker system prune -a  # removes all unused images            # ⚠️

Common Pitfalls

PitfallWhy It HappensFix
No space left on deviceInode exhaustiondf -i, docker system prune
Slow writes to large filesFile-level copy-upUse a volume for write-heavy data
overlay2 not supportedBacking FS is btrfs/zfsUse ext4 or XFS
d_type not supportedXFS without ftype=1Reformat with -n ftype=1
Container start slowDeep layer stackReduce image layers
Disk space growsupperdir accumulatesMonitor, clean, use volumes
Mount failsworkdir on different FSEnsure same filesystem

Real-World Examples

1. Check Driver

docker info | grep "Storage Driver"
# Storage Driver: overlay2

2. View Overlay Mounts

mount | grep overlay

3. Check Inodes

df -i /var/lib/docker
# Filesystem      Inodes  IUsed  IFree IUse% Mounted on
# /dev/sda1      6553600  98120 6455480    2% /

4. Clean Up

docker system prune -a --volumes

5. Configure overlay2

{
  "storage-driver": "overlay2",
  "storage-opts": ["overlay2.mountopt=nodev,metacopy=on"]
}

6. Check XFS ftype

xfs_info /var/lib/docker | grep ftype
# ftype=1

7. Format XFS with ftype

mkfs.xfs -n ftype=1 /dev/sdb

8. High-Density Inodes

mkfs.ext4 -i 131072 /dev/sdb

9. Benchmark Write

docker run -it --rm ubuntu bash -c "dd if=/dev/zero of=test bs=1G count=1 oflag=dsync"

10. Check Upperdir Size

du -sh /var/lib/docker/overlay2/*/diff

Visual

Overlay2 Layer Stack

┌─────────────────────────────────────────────────────────────┐
│  CONTAINER VIEW (merged)                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  /usr/bin/app      (from image)                     │    │
│  │  /etc/config.yml   (from image)                     │    │
│  │  /data/cache.db    (modified — in upperdir)         │    │
│  │  /tmp/scratch      (new file — in upperdir)         │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  UPPERDIR (writable container layer)                │    │
│  │  /data/cache.db    (copy-up from lower)             │    │
│  │  /tmp/scratch      (new)                            │    │
│  └─────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  LOWERDIR (read-only image layers)                  │    │
│  │  Layer 3: /etc/config.yml                           │    │
│  │  Layer 2: /usr/bin/app                              │    │
│  │  Layer 1: /bin, /lib, etc.                          │    │
│  │  /data/cache.db    (obscured by upperdir)           │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Writes go to upperdir. Lowerdir is never modified.         │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Copy-Up Operation

┌─────────────────────────────────────────────────────────────┐
│  BEFORE WRITE                                               │
│                                                             │
│  lowerdir: /data/large-file (1 GB)                          │
│  upperdir: (empty)                                          │
│                                                             │
│  Container writes 1 byte to /data/large-file                │
│    │                                                        │
│    ▼                                                        │
│  COPY-UP: entire 1 GB file copied to upperdir               │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  upperdir: /data/large-file (1 GB copy)             │    │
│  │  lowerdir: /data/large-file (original, unchanged)   │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Write modifies the copy. Lowerdir is untouched.            │
│  Slow for large files. Fast for small files.                │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Inode vs Disk Space Exhaustion

┌─────────────────────────────────────────────────────────────┐
│  DISK SPACE EXHAUSTION                                      │
│                                                             │
│  df -h /var/lib/docker                                      │
│  Filesystem  Size  Used Avail Use%                          │
│  /dev/sda1   100G   99G  1.0G  99%                          │
│                                                             │
│  Fix: delete files, prune images                            │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  INODE EXHAUSTION                                           │
│                                                             │
│  df -i /var/lib/docker                                      │
│  Filesystem  Inodes IUsed IFree IUse%                       │
│  /dev/sda1   6.5M   6.5M  0     100%                        │
│                                                             │
│  df -h shows space available, but files cannot be created.  │
│                                                             │
│  Fix: docker system prune, reformat with higher inode density│
│                                                             │
└─────────────────────────────────────────────────────────────┘

Driver Performance Comparison

┌─────────────────────────────────────────────────────────────┐
│  CONTAINER START TIME                                       │
│                                                             │
│  overlay2  ████████ 50ms                                     │
│  btrfs     ████████████████ 80ms                             │
│  zfs       ██████████████████████ 90ms                       │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  LARGE FILE MODIFICATION (CoW)                              │
│                                                             │
│  overlay2  ████████████████████████████████████ Slow         │
│  btrfs     ████████ Fast                                     │
│  zfs       ████████ Fast                                     │
│                                                             │
│  overlay2 copies the entire file.                           │
│  btrfs and zfs copy only changed blocks.                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Default driveroverlay2
Backing filesystemsext4, XFS with ftype=1
Kernel requirement4.0+
Layers supported128
Copy-on-writeFile-level
Container start~50ms
WeaknessLarge-file modifications
StrengthStartup, general I/O
Inode fixdf -i, docker system prune
Config file/etc/docker/daemon.json

Key takeaways:

  • overlay2 is the default storage driver on most Linux distributions. It uses OverlayFS to stack read-only image layers beneath a writable container layer, presenting a unified view through the merged directory. The image layers are the lowerdir; the container layer is the upperdir .
  • Writes trigger a file-level copy-up. The first time a container writes to a file from the image, the entire file is copied to the container layer. This is fast for small files and slow for large files. Subsequent writes operate on the copy .
  • Inode exhaustion is the most common storage failure. The error is No space left on device, but df -h shows free space. The fix is df -i and docker system prune .
  • overlay2 requires d_type support. On XFS, the filesystem must be formatted with ftype=1. Without it, overlay2 degrades to a slower mode .
  • Container startup is fastest with overlay2. At around 50ms, it beats btrfs and zfs. Its weakness is large-file copy-up, which is significantly slower than block-level drivers like btrfs and zfs .
  • The upperdir is the container’s writable layer. It accumulates files as the container runs. For write-heavy workloads, use volumes instead of the container’s writable layer. Volumes bypass the storage driver and are faster and persistent .
  • Reduce image layers to reduce overhead. Deep layer stacks slow down file lookups. Use multi-stage builds and combine RUN commands in the Dockerfile .

Remember: overlay2 is the workhorse of Docker storage. It provides the best balance of performance and compatibility for most workloads, and it is the driver you will encounter in the vast majority of installations. Its architecture is a union mount of read-only image layers and a writable container layer. Its copy-up behavior is file-level, which is fast for startup and small files but slow for large-file modifications. Its failure mode is inode exhaustion, which masquerades as disk space exhaustion. Monitor df -i, clean up unused resources, use volumes for write-heavy data, and reduce image layers. The driver is not something you configure once and forget—it is something you monitor and manage.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!