Skip to content
Containers

Docker Storage Drivers: overlay2 vs btrfs vs zfs Performance

overlay2 is the right default for 95% of workloads. btrfs wins on snapshot-heavy flows. zfs wins on multi-tenant dedup. Real fio benchmarks and migration paths.

A
Abhishek Patel10 min read

Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

Docker Storage Drivers: overlay2 vs btrfs vs zfs Performance
Docker Storage Drivers: overlay2 vs btrfs vs zfs Performance

Docker Storage Drivers: When overlay2 Isn't Enough

The disk I/O penalty is the largest perf gap between Docker and bare metal — overlay2 (Docker's default) costs 7-14% on write-heavy workloads versus running directly on the host filesystem. Most teams accept that overhead and move on; some discover too late that they should have picked a different driver. overlay2 is the right default for 95% of workloads — fast, well-understood, broadly supported. btrfs wins for snapshot-heavy workflows where you're constantly creating, destroying, and rolling back container state. zfs wins for large-scale storage management with deduplication and compression built in. The honest rule: pick overlay2 unless you have a specific reason for the others, and stop trying to use containers for production database workloads where bind mounts are the right answer.

DriverSequential writeRandom writeSnapshot speedBest forDon't use for
overlay292% of host86% of hostfast (CoW)Default; most workloadsHeavy random write to overlay layers
btrfs88% of host82% of hostvery fastSnapshot-heavy, ML training checkpointsOlder kernels (pre-5.10)
zfs78% of host72% of hostvery fastStorage with dedup + compressionMemory-constrained hosts
devicemapper (deprecated)Legacy compatibility onlyAnything new
vfs (testing)40% of host35% of hostn/a (no CoW)Debugging onlyProduction

Last updated: April 2026 — measured on Ubuntu 24.04 LTS with kernel 6.8, Docker 27.x, fio benchmarks against NVMe Gen4 SSD. Numbers shift slightly across kernels and Docker versions.

How overlay2 Actually Works (and Why It Has Overhead)

Container images are layered: each RUN in a Dockerfile creates a new read-only layer. When you run a container, overlay2 stacks these layers using the kernel's overlay filesystem, plus a writable "upperdir" layer for runtime changes. Reading any file walks down the stack until it's found; writing forces a copy-on-write (CoW) operation that copies the file into the upperdir before modifying it.

The CoW penalty is where overlay2 loses to bare metal. Three failure modes:

  • First-write of large files is slow because the entire file copies before the modification. Editing a 100 MB log file inside a container costs 100 MB of disk I/O before the first byte writes.
  • fsync-heavy workloads hit overlay2's metadata path harder than direct filesystem access. Postgres, MySQL, anything with frequent durable writes — overlay2 measurably slower than bind mounts.
  • Many small writes to the same file trigger CoW once but then run at near-native speed. The first write is the expensive one.

The Docker vs bare metal benchmarks cover the broader CPU/memory/disk picture. The point here: overlay2's overhead is real but bounded, and bind mounts are the escape hatch.

The Bind Mount Escape Hatch

For any I/O-heavy directory inside a container, mount a host directory directly:

services:
  postgres:
    image: postgres:18
    volumes:
      - /var/lib/postgres-data:/var/lib/postgresql/data

The /var/lib/postgres-data path on the host completely bypasses overlay2 — Postgres reads and writes at native filesystem speed. Use bind mounts for: databases, log directories, ML training checkpoints, anything generating sustained disk I/O. The container layer holds binaries and config; the bind mount holds the data.

Pro tip: The single biggest win on Docker disk performance is moving heavy I/O paths to bind mounts. Doesn't require switching storage drivers, doesn't require kernel changes — just the right docker-compose configuration. Podman handles bind mounts identically.

btrfs: Snapshot-Heavy Workloads

btrfs is a copy-on-write filesystem with native snapshots, subvolumes, and online deduplication. The Docker btrfs driver maps each container layer to a btrfs subvolume — instant snapshot, instant rollback, instant clone. For ML training where you're constantly checkpointing model state, or CI workflows that snapshot the build environment between stages, btrfs is dramatically faster than overlay2 + tar.

When btrfs wins

  • ML training with frequent checkpoints: snapshot a 5 GB model state in milliseconds vs seconds for tar.
  • CI/CD with shared base layers: dedup means 50 copies of the same Node modules use disk for one. Real savings on storage-constrained build farms.
  • Database backup workflows: btrfs snapshot the data dir, copy at leisure, no application downtime.

When btrfs hurts

  • Older kernels (pre-5.10): btrfs has had stability issues historically. On modern kernels (6.x), it's solid.
  • Workloads that fill the disk: btrfs handles space pressure poorly compared to ext4. Keep at least 15% free.
  • Workloads with high random write: 4-6% slower than overlay2 + ext4 in our fio measurements.

zfs: Enterprise Storage Features

ZFS brings deduplication, compression, snapshots, and RAID-Z to the Docker storage layer. Each container becomes a ZFS clone of its parent layer. The catch: ZFS is memory-hungry — recommended ARC (cache) is 1 GB per TB of storage, and dedup tables can dominate RAM on small hosts.

When zfs wins

  • Multi-tenant container hosts with similar images: dedup savings can be 60-80% on disk for typical Docker workloads where many containers share Ubuntu/Alpine bases.
  • Compression-friendly workloads: lz4 compression often hits 1.5-2x ratio on container layers, freeing meaningful space.
  • You already run ZFS: if your host is on TrueNAS, Proxmox, or you've adopted ZFS as the OS filesystem, the Docker zfs driver integrates cleanly.

When zfs hurts

  • Memory-constrained hosts: ZFS's ARC can balloon to 50%+ of system RAM. Bad on a 16 GB Pi-class host.
  • Sustained sequential I/O: 7-15% slower than overlay2 in pure throughput. Not great for CI build artifacts.
  • Solo developers without ZFS expertise: ZFS has its own operational model. Wrong defaults can corrupt data.

How to Switch Drivers (and Why You Probably Shouldn't)

Docker reads the storage driver from /etc/docker/daemon.json:

{
  "storage-driver": "btrfs",
  "data-root": "/var/lib/docker"
}

Switching drivers requires migrating data — Docker doesn't convert layers between drivers. The clean process:

  1. docker save all images you want to keep to tar files.
  2. Stop Docker, remove /var/lib/docker.
  3. Update daemon.json, restart Docker.
  4. docker load the saved images back.

This is disruptive enough that you shouldn't do it casually. The right time to pick a non-default driver is when you set up a new host, not when you migrate existing workloads. The rootful vs rootless Docker comparison covers another major Docker config decision that's similarly best made upfront.

fio Benchmark Methodology

Numbers in this article are from fio runs on a Ryzen 9 7950X with 64 GB DDR5 and a Samsung 990 Pro 2 TB NVMe Gen4 SSD. fio config:

# Sequential write
fio --name=seqwrite --ioengine=libaio --direct=1 \
  --bs=1M --size=4G --numjobs=1 --rw=write --runtime=60s

# Random write
fio --name=randwrite --ioengine=libaio --direct=1 \
  --bs=4k --size=2G --numjobs=4 --rw=randwrite --runtime=60s

# fsync workload (database-style)
fio --name=fsyncrw --ioengine=libaio --direct=1 --fsync=1 \
  --bs=4k --size=2G --numjobs=4 --rw=randrw --runtime=60s

Each run executed three times; reported numbers are medians. Bare-metal baseline is the same drive without Docker. Container runs use a Postgres-like fio profile inside a long-running container (no first-write CoW penalty), which is why overlay2 looks decent in the table — the cold-start CoW hit doesn't show up in steady-state benchmarks. If your workload is dominated by container-image churn rather than steady-state I/O, expect overlay2 overhead to look closer to 15-25% than the 8-14% steady-state numbers.

Watch out: Storage driver choice interacts with Docker's image-build cache invalidation logic. With overlay2, image rebuilds reuse cached layers when possible. With btrfs/zfs, the cache layer behavior is slightly different and can occasionally cause spurious cache misses or extra disk usage. Stick with overlay2 if you're CI-build sensitive.

Real Recommendations Per Workload

  • Web app + database in containers (typical SaaS): overlay2 + bind mount the database data directory. The right answer for 90% of teams.
  • ML training cluster with frequent checkpoints: btrfs on host filesystem, save checkpoints to btrfs subvolumes. Snapshots are your friend.
  • Multi-tenant container PaaS: zfs for the dedup/compression savings. Plan ARC sizing carefully.
  • Single-developer laptop: overlay2 default. Don't optimize what doesn't need optimizing.
  • CI build farm: overlay2 default + aggressive layer caching. The build-time wins are in multi-stage builds, not storage driver.
  • High-IOPS database in production: don't run the database in Docker. Use a managed Postgres / MySQL service or run on bare metal. Containers are not the right tool for this job.

Frequently Asked Questions

What is the best Docker storage driver?

overlay2 is the right default for 95% of workloads — fast, well-supported, minimal overhead in steady-state operation. Use btrfs when you need fast snapshots (ML training, CI flows). Use zfs when you need deduplication and compression on multi-tenant hosts. Don't use devicemapper (deprecated) or vfs (testing only) in production.

How much overhead does Docker overlay2 add?

For sequential write to bind-mounted volumes: under 2%. For overlay2 layer writes (in-container filesystem): 8-14% in steady state, 15-25% during first-write CoW operations. For fsync-heavy workloads (databases): 10-18%. The single biggest mitigation is bind-mounting heavy I/O paths to the host filesystem, which bypasses overlay2 entirely.

Should I use btrfs for Docker?

Only if your workload benefits from fast snapshots — ML training with frequent checkpoints, CI flows with shared base layers, snapshot-based backups. For typical web app + database workloads, overlay2 is faster and simpler. btrfs has a stable kernel implementation in 6.x but stays a minority choice. Don't switch from overlay2 unless you have a specific reason.

What's the difference between overlay2 and bind mount?

overlay2 is Docker's storage driver for container filesystem layers — handles the union-mount of image layers plus a writable runtime layer. Bind mounts are direct host-filesystem mounts inside a container that bypass overlay2 entirely. For I/O-heavy paths (databases, logs, ML training data), use bind mounts to avoid the overlay2 CoW penalty. Both can coexist in the same container.

Is ZFS good for Docker?

For multi-tenant hosts with many similar containers, yes — deduplication can save 60-80% disk space and lz4 compression adds another 1.5-2x. For single-purpose hosts or memory-constrained environments, no — ZFS's ARC cache requirement (1 GB RAM per TB storage) and dedup table memory cost outweigh the savings. ZFS is also slower on raw sequential I/O (78% of host vs overlay2's 92%).

Why is Docker disk I/O slow?

The biggest cause: writing to overlay2 layers triggers copy-on-write, which copies entire files before modification. Compounded by fsync-heavy workloads (databases) hitting overlay2's metadata path harder. Fixes: bind-mount heavy I/O paths to host filesystem, use --tmpfs for ephemeral data, switch to a faster storage driver if your workload pattern matches (btrfs for snapshots, zfs for multi-tenant dedup).

How do I change Docker storage driver?

Edit /etc/docker/daemon.json to set "storage-driver": "btrfs" (or zfs), then restart Docker. The catch: existing image layers don't migrate — you'll lose them. Process: docker save images you want, stop Docker, remove /var/lib/docker, update daemon.json, restart, docker load images back. Best done on new host setup, not on running production.

Stick with overlay2 + Bind Mounts

For 95% of Docker workloads, the answer is overlay2 with bind mounts on heavy I/O paths. The exotic storage drivers exist for genuine use cases — btrfs for snapshot-heavy workflows, zfs for multi-tenant dedup — but most teams who switch to them discover they didn't actually need to. The bigger wins on Docker performance are in image size (multi-stage builds), workload separation (databases on bare metal or managed services), and right-sizing the storage path. Pick overlay2 by default; switch only when you have a measured reason.

A

Written by

Abhishek Patel

Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.

Related Articles

Enjoyed this article?

Get more like this in your inbox. No spam, unsubscribe anytime.

Comments

Loading comments...

Leave a comment

Stay in the loop

New articles delivered to your inbox. No spam.