overlay2 Internals: How Docker Really Stores and Runs Your Containers
A deep, from-first-principles walkthrough of Docker's default storage driver — how layers are built, how writes are captured, why some images boot in milliseconds, and where the design quietly breaks down.
Imagine a photocopier that never actually copies the original page. Instead, every time someone wants to “edit” a document, the copier hands them a transparent sheet to scribble on, and stacks that sheet on top of the original. Anyone reading the document sees the original page with the scribbles on top — but the original itself is never touched, so a hundred people can each get their own transparent sheet over the exact same original, without a hundred photocopies being made. That transparent-sheet trick is, in essence, what Docker’s overlay2 storage driver does with your filesystem, and understanding it is the key to understanding why containers start in milliseconds, why images share space on disk, and why a container’s writes vanish the moment you delete it. By the end of this tutorial, the goal isn’t just to recognize the name “overlay2” — it’s to be able to look at any container storage symptom, from a mysteriously full disk to a slow database write, and reason directly back to the exact mechanism causing it.
1What overlay2 Actually Is
Before touching any internals, it helps to place overlay2 correctly: it is not Docker, it is not a filesystem you format a disk with, and it is not unique to Docker.
The one-sentence definition
overlay2 is Docker’s default storage driver on Linux — a piece of software that tells the Docker engine how to store image layers and container filesystems on disk using the Linux kernel’s built-in OverlayFS filesystem. It is not something you install separately; it has shipped inside the Linux kernel since version 3.18 (merged, stable form arrived around 4.0+), and Docker simply calls into it through kernel mount syscalls.
Think of overlay2 as the “sticky note” system for a shared textbook. The textbook (a Docker image) is printed once and never altered. When you (a container) want to write in the margins, you don’t get your own printed copy — you get a stack of transparent sticky notes placed over the pages. You can write, cross out, or “hide” printed text using your sticky notes, but the underlying textbook page is untouched, so your friend reading the same textbook with a different sticky-note stack never sees your notes.
OverlayFS
The actual filesystem implementation living inside the Linux kernel that merges multiple directories into one unified view.
overlay2 driver
Docker’s storage-driver code that decides directory layout, naming, and how images/containers map onto OverlayFS mounts.
overlay (legacy)
An older, less efficient driver superseded by overlay2 once the kernel gained multiple lower-directory support.
aufs, devicemapper, btrfs, zfs, fuse-overlayfs
Alternative storage drivers Docker also supports, each with different trade-offs covered later in this tutorial.
The original overlay driver could only combine exactly two directories (one lower, one upper). Kernel 4.0 added support for multiple lower directories, which is what makes efficient, multi-layer image sharing possible — Docker’s driver for that capability is called overlay2 to distinguish it from the original.
A brief history, in plain terms
OverlayFS itself started life as an out-of-tree kernel patch used by early Linux live-CD and embedded-systems projects, long before containers were mainstream — those projects needed a cheap way to let a read-only base system appear “writable” during a live session. Docker’s earliest releases actually shipped with the older, patch-dependent aufs driver instead, because at the time OverlayFS wasn’t yet merged into mainline Linux. Once OverlayFS landed in the mainline kernel and later gained multi-lowerdir support, Docker migrated its default to overlay2, and the ecosystem has broadly stayed there ever since.
2Core Concepts: Union Filesystems
overlay2 belongs to a family of technologies called union filesystems (or union mounts). Understanding this family explains almost everything that follows.
What is a union filesystem?
A union filesystem lets you take several separate directories, stack them logically, and present them to the operating system as one single, merged directory — without physically copying any files between them. If the same filename exists in two of the stacked directories, the topmost one “wins” and is the one users see. This merging happens live, in the kernel, every time a process reads that directory.
Picture three transparent slides on an overhead projector, each with different drawings. When stacked and projected, you see one combined picture — not three separate images. If two slides both have something drawn in the same corner, only the slide on top is visible in that spot. Union filesystems do exactly this with directories of files instead of slides.
The four directories overlay2 juggles
Every single OverlayFS mount — whether for an image layer or a running container — is built from four directory roles. Learning these four names is non-negotiable for understanding overlay2, because every log message, every troubleshooting guide, and every docker inspect output refers to them directly.
lowerdir
One or more stacked, read-only directories — these are the image layers. Nothing here is ever modified after it’s created.
upperdir
A single writable directory that captures every change a running container makes — new files, edits, deletions.
merged
The unified directory a container’s process actually sees as its root filesystem — the “projected image” from our slide analogy.
workdir
A scratch directory the kernel uses internally to safely prepare file operations before committing them into upperdir. Applications never touch it directly.
People often assume a running container “has its own filesystem copy.” It doesn’t. A container is really just an upperdir plus a read-only stack of lowerdirs, merged live. Nothing is duplicated until something is actually written.
How directory listings actually get merged
When a process asks to list the contents of a directory in the merged view, the kernel doesn’t just show the topmost layer’s version and stop — it walks every lowerdir that also contains a directory of the same name, at the same path, and combines all of their entries into one listing, with upperdir’s version of any given filename taking priority whenever the same name appears more than once. A directory that has never been touched by the container might therefore be assembled, live, from pieces scattered across several different image layers, all without the kernel ever writing a merged copy back to disk — the merge is computed on every access, not stored.
It’s like several people independently filling in different rows of a shared seating chart. No single sheet has the whole picture on its own, but a glance across every sheet at once — checking the topmost sheet first for any seat number — tells you exactly who is sitting where.
3Architecture & Components on Disk
All of this theory lives at a very real, very inspectable location on your machine: /var/lib/docker/overlay2.
The directory Docker actually maintains
When Docker is configured to use overlay2, every image layer and every container gets its own subdirectory under /var/lib/docker/overlay2/, named with a long random-looking hash (its layer ID). Inside each of those hash-named folders, Docker keeps the pieces OverlayFS needs.
| Path (relative) | Role | Exists for |
|---|---|---|
<layer-id>/diff | The actual file content added by this layer | Every image layer and every container |
<layer-id>/link | A short symlink alias to avoid huge mount arguments | Every layer |
<layer-id>/lower | A text file listing the short names of parent layers below this one | Every non-base layer |
<layer-id>/work | The kernel’s internal workdir scratch space | Only container (writable) layers |
<layer-id>/merged | The unified mount point — the container’s visible root filesystem | Only running containers |
l/ (top-level) | A folder of short symlinks pointing back into diff folders | Docker-wide, shared by all layers |
l/ exist
Linux mount syscalls have a maximum argument length (typically one page, ~4KB). A container built on 20+ layers would produce a lowerdir string of dozens of full 64-character hash paths, easily blowing past that limit. Docker works around this by creating a short (~26 character) symlink for every layer inside a shared l/ directory, then builds the mount option string out of those short symlinks instead of the long paths.
graph TD
A["/var/lib/docker/overlay2/"] --> B["l/ (short symlinks)"]
A --> C["<layer-id-1>/diff (base OS files)"]
A --> D["<layer-id-2>/diff (app dependencies)"]
A --> E["<layer-id-3>/diff (app code)"]
A --> F["<container-id>/diff (writable layer)"]
A --> G["<container-id>/work (kernel scratch)"]
A --> H["<container-id>/merged (unified view container sees)"]
B -. "short alias" .-> C
B -. "short alias" .-> D
B -. "short alias" .-> E
H --> F
H --> E
H --> D
H --> C
The identity trio: RepoDigests, DiffIDs, and layer IDs
It’s worth knowing that the hash-named folder you see on disk is not the same hash you see with docker images. An image layer actually has three related identities: a content hash for the compressed layer as distributed over the network, a “DiffID” hash for the uncompressed content, and the local overlay2 cache-id used purely for the folder name on this particular machine. Docker keeps a small internal database mapping between all three so it never has to re-download or re-extract a layer it already has.
The metadata database backing all of this
Alongside the layer folders themselves, Docker’s image and layer store maintains its own small embedded database (historically BoltDB-based, under /var/lib/docker/image/overlay2/) that records, for every layer, which parent it stacks on, its DiffID, its size, and its local cache-id. This is the piece of bookkeeping that lets commands like docker history reconstruct a full, ordered layer chain for an image, and it’s exactly why manually deleting a layer folder by hand — bypassing Docker entirely — is dangerous: the database still thinks the layer exists, and a wide range of subsequent operations can fail or misbehave once the two disagree.
Treating /var/lib/docker/overlay2/ as “just a folder you can clean up with rm -rf.” The directory tree and Docker’s internal metadata database must always agree; manual edits are one of the most common ways people accidentally corrupt a Docker installation’s image store.
4Internal Working: Copy-Up and Whiteouts
Two mechanisms — copy-up and whiteout files — do essentially all of the heavy lifting inside overlay2. Master these two and the rest of the driver’s behavior becomes predictable.
Copy-up: how “editing” a read-only file actually works
Since lowerdirs are strictly read-only, what happens when a process inside a container tries to modify a file that currently lives in a lower (image) layer? The kernel performs an operation called copy-up: it copies the entire file, unmodified, from whichever lowerdir it currently lives in, up into the container’s upperdir — and only then applies the requested change to that new copy. From that point on, every read of that file is served from the upperdir copy, because upperdir always takes priority in the merged view.
It’s like a library book you’re not allowed to write in. If you want to annotate a page, the librarian photocopies that one page for you, hands you the photocopy, and from then on you scribble on your personal photocopy while the original book on the shelf stays pristine. Anyone else who checks out the same book still sees a clean page.
Copy-up operates on the whole file, not just the changed bytes. Modifying one byte inside a 2GB log file that lives in a lower layer forces the kernel to copy the entire 2GB file into upperdir first. This is one of the most common real-world overlay2 performance surprises, especially with databases or large log files stored inside the image layers instead of a mounted volume.
Whiteout files: how deletion is faked
Because lowerdirs can never be modified, overlay2 can’t literally erase a file that exists in a lower layer. Instead, when a container “deletes” such a file, the kernel creates a special marker inside upperdir called a whiteout — a character device file with device number 0/0, sharing the same name as the file being hidden. When OverlayFS builds the merged view, it sees the whiteout marker and simply omits that filename from the presented listing, even though the original still physically exists, untouched, down in the lowerdir.
It’s the sticky-note equivalent of covering a printed line with correction tape. The original printed word is still there under the tape — you haven’t erased ink from the textbook — but visually, to anyone reading through the sticky-note layer, that word is gone.
Opaque directories: hiding an entire folder
A related trick handles the case of deleting or replacing an entire directory rather than a single file. overlay2 marks a directory as opaque by setting a special extended attribute (trusted.overlay.opaque=y) on the upperdir copy of that folder. An opaque directory tells the kernel to stop looking further down into any lowerdirs for that folder’s contents — effectively resetting it, which is exactly the behavior Dockerfile instructions like removing and recreating a directory rely on.
A note on hard links across layers
Hard links are files that share the same underlying data on a native filesystem, and they behave a little differently once layers get involved. A hard link created entirely within a single layer works exactly as expected. But if a file that already exists in a lower layer gets a new hard link created against it from inside a running container, the kernel can’t create a true hard link that spans a read-only lower layer and a writable upper layer — instead, it performs a copy-up of the target file into upperdir first, and the new link is created against that upperdir copy. The result is correct from the application’s point of view, but it’s another situation, alongside plain file edits, where an operation that looks cheap on a native filesystem quietly costs a full copy-up on overlay2.
sequenceDiagram
participant P as Process in container
participant M as merged view
participant U as upperdir
participant L as lowerdir (image layer)
P->>M: write("/etc/config.txt", newData)
M->>U: does config.txt exist in upperdir?
U-->>M: no
M->>L: copy-up config.txt (full file)
L-->>U: file copied into upperdir
M->>U: apply write to upperdir copy
U-->>P: write succeeds
Note over L: original in lowerdir is never touched
5Data Flow & Lifecycle
Tracing a container from docker pull to docker rm shows exactly when and where overlay2 gets involved at each stage.
Image pull
Docker downloads each compressed layer, verifies its digest, and extracts it into its own diff folder under /var/lib/docker/overlay2/. No mounting happens yet — these are just plain directories of files sitting on disk.
Container creation
Docker creates a brand-new writable layer folder for this specific container, containing empty diff and work subdirectories, and writes a lower file listing every image layer’s short symlink, in order.
Container start
Docker issues the actual OverlayFS mount syscall, combining all the lowerdirs (image layers, read-only) with the new upperdir (writable) and workdir, and exposing the result at merged. This merged path becomes the container’s root filesystem.
Runtime reads & writes
Reads are served from whichever layer — upper or lower — has the most recent copy of a file. Writes always land in upperdir, triggering copy-up for any pre-existing file being modified for the first time.
Container stop
The OverlayFS mount is torn down, but the upperdir folder on disk is left exactly as it was — nothing is deleted. A stopped container can be started again and will see the same writable state.
Container removal
Only now does Docker delete the container’s upperdir, workdir, and merged folders from disk. The underlying image layers are untouched and remain available for any other container built from the same image.
Everything a container writes lives only in its own upperdir. The moment step 6 happens, that upperdir — and everything unique the container ever wrote — is gone forever, unless the data was explicitly placed in a Docker volume or bind mount, which live outside the overlay2 layer stack entirely.
The optional seventh step: turning a container’s changes into a new image
There’s one more transition worth understanding, even though it happens less often in normal workflows: committing a running or stopped container into a brand-new image. When this happens, Docker takes everything currently sitting in that container’s upperdir — including any whiteout markers recording deletions — and packages it as a fresh, standalone image layer, placed on top of the exact lower layers the container was already using. That new layer gets its own content digest and can now be pushed, shared, or used as the base for further containers, exactly like any layer that originated from a Dockerfile instruction. It’s the same copy-up-and-whiteout machinery from earlier chapters, just observed from the image-authoring side rather than the runtime side.
6Layer Sharing & Image Composition
This chapter explains the payoff of the whole design: why ten containers built from the same base image barely use any extra disk space, and why docker build caching is so effective.
Content-addressable layers
Every image layer is identified by a cryptographic hash of its own content. This means if two completely different images both happen to start with, say, the exact same Ubuntu base layer, Docker recognizes the identical hash and stores that base layer’s files exactly once on disk — both images simply reference the same diff folder as one of their lowerdirs.
It’s like a shared corporate template library. A hundred different PowerPoint decks across a company can all “use” the same company-logo slide master without each deck secretly containing a hundred separate copies of that slide — they just all point back to the one shared master file.
Why docker build caching works
Each Dockerfile instruction that changes the filesystem (RUN, COPY, ADD) produces one new layer, hashed from its inputs. If you rebuild an image and an earlier instruction’s inputs haven’t changed, Docker reuses that layer’s existing hash from cache instead of re-executing it — which is exactly why Dockerfile authors are taught to put frequently-changing instructions (like copying application source code) near the bottom of the file: it maximizes how many layers above it stay cache-hit on every rebuild.
From manifest to mount: how a chain of hashes becomes a stack of directories
An image’s manifest — the JSON document that a registry actually serves when you pull an image — lists its layers purely as an ordered array of content digests; it says nothing about local folder names at all. When Docker pulls an image, it walks that list top to bottom, and for each digest checks its local metadata database for a matching, already-extracted layer before downloading anything. Only missing layers are actually fetched and decompressed. Once every layer in the chain is present locally, Docker resolves each digest to its local cache-id, and it’s only that final list of cache-ids that ever gets handed to the kernel as the overlay2 mount’s lowerdir string.
| Stage | What identifies a layer here |
|---|---|
| Registry manifest | Distributable content digest (what you’d verify against a signature) |
| Local image store | DiffID (hash of the uncompressed layer content) |
| overlay2 on-disk folder | cache-id (host-local, arbitrary, stable only on this machine) |
| Kernel mount syscall | Short symlink name inside l/ (to fit the mount argument length limit) |
This layered chain of identities is what allows the same conceptual “layer” to be verified cryptographically when transferred over the network, deduplicated logically inside Docker’s own bookkeeping, and represented as a short, filesystem-friendly name at the point the kernel actually needs to build a mount — each identity is optimized for the concern at the stage it’s used.
7Where the Design Breaks Down: Limits & Edge Cases
overlay2 is elegant, but it inherits real constraints from both the Linux kernel and the filesystem underneath it. Knowing these limits in advance saves painful production debugging later.
Max lowerdir stack depth
Older kernels capped the number of stackable lowerdirs at 128; exceeding it via extremely long, uncollapsed Dockerfiles or deep multi-stage builds can fail a container’s start.
Page-size mount string
Even with the short-symlink workaround in l/, an extreme number of layers can still overflow the kernel’s mount option string limit.
Needs d_type support
overlay2 requires the underlying filesystem (typically ext4 or xfs) to support directory entry type info (d_type). Some older or misconfigured xfs setups disable this and silently break overlay2.
Not fully POSIX-identical
A few rare operations — like certain hard-link and rename edge cases across layer boundaries — don’t behave identically to a native filesystem, which occasionally surprises very filesystem-sensitive applications.
Running databases with their data directory left inside the container’s writable layer (instead of a bind mount or named volume). Beyond the “data vanishes on docker rm” problem, heavy random-write database workloads on overlay2’s copy-up mechanism are noticeably slower than on a native filesystem — this is precisely why official database images document a VOLUME for their data path.
Filesystem feature requirements, explained
The d_type requirement mentioned above deserves a plain-English explanation, because it trips people up in the wild more often than the layer-count limit does. When the kernel scans a directory, each entry can optionally carry a quick hint about what kind of object it is — a regular file, a directory, a symlink — without needing a separate, more expensive lookup call to find out. OverlayFS relies on this hint being present and accurate to correctly merge entries across layers and to know when a whiteout marker is standing in for a deleted directory versus a deleted file. Some older xfs filesystems were formatted with this feature turned off by default, and Docker will refuse to use overlay2 on top of such a filesystem, reporting an explicit error rather than risking silently incorrect behavior.
Nested containers: overlay-on-overlay
A less commonly discussed limitation shows up in Docker-in-Docker setups, where a container itself runs a Docker daemon that also wants to use overlay2 for its own, inner containers. Historically, the Linux kernel restricted or outright refused using an OverlayFS mount as the lowerdir input to another OverlayFS mount, since the underlying filesystem already lacks certain low-level features (like fully generic extended-attribute passthrough) that a second overlay layer depends on. In practice this means nested Docker daemons commonly need to be configured with a different storage driver, or run against a dedicated volume formatted with a fully-featured filesystem, rather than simply inheriting overlay2 by default.
8Advantages, Disadvantages & Trade-offs
overlay2 became Docker’s default for good reasons, but “default” doesn’t mean “best for every workload.” Here’s the honest balance sheet, followed by how it stacks up against the alternatives.
Advantages
- Native kernel implementation — no extra userspace daemon or kernel module to install, unlike devicemapper or zfs on Linux.
- Very fast container startup, since starting a container is “just” a mount syscall, not a data copy.
- Efficient shared storage — identical layers across many images and containers are stored once.
- Simple, human-readable on-disk layout that’s straightforward to inspect and back up.
- Page cache friendly — because files are read through a normal mount, the kernel’s page cache works transparently, unlike some block-based drivers.
Disadvantages / Trade-offs
- Whole-file copy-up penalty for large files modified in place.
- Layer-count and mount-argument ceilings on very deep image histories.
- Slightly non-standard semantics for some rename/hard-link edge cases.
- Heavy random-write workloads (databases, some CI caches) generally perform better on a dedicated volume than directly on overlay2’s writable layer.
How it compares to sibling storage drivers
| Driver | Layer strategy | Requires special setup? | Typical status today |
|---|---|---|---|
| overlay2 | Kernel union filesystem (OverlayFS) | No — works on ext4/xfs out of the box | Default on virtually all modern Linux distros |
| aufs | Older union filesystem, predecessor concept | Requires a separate kernel patch/module | Largely retired in favor of overlay2 |
| devicemapper | Block-level snapshots (thin provisioning) | Needs dedicated block device / loopback setup | Legacy; mostly seen on very old RHEL/CentOS systems |
| btrfs | Native filesystem-level subvolume snapshots | Requires a btrfs-formatted /var/lib/docker | Niche, used when the host already runs btrfs |
| zfs | Native filesystem-level snapshots & clones | Requires ZFS kernel module + zpool | Popular in storage-focused or FreeBSD-adjacent shops |
| fuse-overlayfs | Userspace re-implementation of OverlayFS | No root privileges required | Used for rootless Docker/Podman |
A mental model for reasoning about performance trade-offs
Rather than memorizing specific benchmark numbers — which shift with kernel version, disk hardware, and workload — it’s more durable to reason from the mechanism itself. overlay2 excels whenever a workload’s dominant pattern is “read a lot, occasionally write small, mostly-new files,” because reads hit the shared page cache and writes stay contained to a single small upperdir. It struggles relatively more whenever a workload repeatedly rewrites large, pre-existing files in place, because each first-touch triggers a full copy-up. Framed this way, the earlier database and large-log-file warnings aren’t arbitrary rules of thumb — they’re direct consequences of this one mechanism.
Network filesystem incompatibility
One more practical constraint worth flagging here: OverlayFS has long had limited, inconsistent, or outright unsupported behavior when either its lowerdir or upperdir sits on a network filesystem such as NFS. This is a direct consequence of certain filesystem features (extended attributes, consistent d_type reporting, specific locking semantics) that OverlayFS depends on but that network filesystems don’t always implement identically to a local one. Production Docker hosts are expected to keep /var/lib/docker on genuine local storage for this reason.
9Security Considerations
Storage drivers aren’t usually top-of-mind for container security, but overlay2’s behavior has direct, practical consequences.
Secrets baked into a layer never truly disappear
If a Dockerfile RUN instruction writes a secret file and a later instruction deletes it, overlay2 has already committed that secret into an earlier layer’s diff folder. The whiteout in the later layer only hides it from the merged view — the raw bytes remain fully extractable from the image tarball. This is exactly why multi-stage builds and dedicated secret-mounting mechanisms exist.
Capability requirements for privileged operations
Performing an OverlayFS mount traditionally requires elevated privileges on the host, which is one reason classic Docker runs a root-owned daemon. Rootless Docker sidesteps this with fuse-overlayfs, trading a little performance for a meaningfully smaller privilege footprint.
Shared lower layers are a read-only trust boundary
Because many containers can mount the very same lowerdir simultaneously, the read-only guarantee of lowerdirs is a genuine security property, not just an implementation detail — it’s what prevents one compromised container from tampering with a base image layer that other, unrelated containers are also using.
Treat every byte ever written during an image build as permanently public within that image, regardless of later deletions — scan images for secrets, and prefer multi-stage builds or BuildKit’s dedicated secret-mount feature over “write then delete” patterns.
Defense in depth beyond the storage driver
It’s worth being clear-eyed that overlay2, like any storage driver, is implemented in kernel code and has, at various points in its history, been the subject of security patches addressing specific bugs — this is true of essentially every actively developed kernel subsystem, not a special weakness unique to overlay2. The practical takeaway isn’t to distrust overlay2 specifically, but to apply the same baseline hygiene that applies to any kernel-adjacent container component: keep host kernels patched and up to date, run containers with the least privilege they actually need (avoiding unnecessary root or extra capabilities), and treat the storage driver as one layer in a broader defense-in-depth strategy rather than a security boundary on its own.
10Monitoring, Logging & Metrics
overlay2 problems tend to show up as either “disk is unexpectedly full” or “container start is unexpectedly slow.” Both have a small, predictable set of things worth watching.
Per-object breakdown
Docker’s built-in disk-usage reporting separates space used by images, containers (their writable layers), and volumes — the first place to look when overlay2 storage seems to be growing unexpectedly.
Inode exhaustion
Union filesystems with many small files across many layers can exhaust filesystem inodes well before raw disk space runs out — a classic overlay2 surprise on ext4 filesystems formatted with too few inodes.
Dangling layer directories
Interrupted pulls or crashed builds can leave orphaned diff folders under /var/lib/docker/overlay2/ that no image references — periodic pruning reclaims this space.
dmesg & kernel logs
OverlayFS mount failures, d_type incompatibility, and layer-limit errors surface in the kernel ring buffer, not in Docker’s own logs — worth checking first for any “container won’t start” storage error.
| Symptom | Likely overlay2-related cause |
|---|---|
| “No space left on device” despite free disk space | Inode exhaustion on the underlying filesystem |
| Container fails to start with a mount error | Layer stack too deep, or corrupted/missing lower symlink in l/ |
| Disk usage keeps climbing after cleanup | Orphaned layer directories not yet garbage-collected |
| Slow writes inside a specific container only | Large in-layer file repeatedly triggering full copy-up |
Where these metrics surface in day-to-day tooling
Docker’s own disk-usage summary breaks total storage down into images, containers, local volumes, and a build-cache category, along with how much of each is “reclaimable” versus actively in use — this single view is usually the fastest way to see whether unexpected growth is coming from dangling image layers, stopped-but-not-removed containers, or genuinely active data. At a cluster level, tools like cAdvisor and the metrics it feeds into Prometheus-based monitoring stacks report per-container filesystem usage by inspecting each container’s overlay2 upperdir directly, which is how dashboards can show “container X is using Y MB of writable storage” without needing any cooperation from the application running inside that container.
Check disk-usage and prune reports on a schedule rather than only when a host actually runs out of space — overlay2’s efficient sharing means growth is often gradual and easy to miss until an unrelated deploy suddenly tips a host over its limit.
11Design Patterns & Anti-patterns
Most of what separates a lean, fast-building image from a bloated, slow one comes down to how well the author worked with overlay2’s layer model instead of against it.
Problem
Splitting a single logical change across many separate RUN instructions (e.g. one RUN apt-get update, another RUN apt-get install, another RUN rm -rf /var/lib/apt/lists/*).
Why It’s Harmful
Each instruction becomes its own layer. The cleanup in the final RUN only whiteouts the files in the merged view — the package cache from the earlier layer is still physically present on disk and still gets shipped inside the image.
Correct Approach
Chain related filesystem-changing commands into a single RUN instruction with && so the download, install, and cleanup all happen inside one layer’s diff, keeping the temporary files out of the final image entirely.
Problem
Copying the entire application source tree, including dependency manifests, in one COPY . . instruction near the top of the Dockerfile.
Why It’s Harmful
Any source file change — even a one-line comment — invalidates that layer’s hash, and with it every layer built on top, including the expensive dependency-install step, forcing a full re-download/re-install on every rebuild.
Correct Approach
Copy only the dependency manifest first, run the install, and copy the rest of the source afterward — so dependency layers stay cache-hit across ordinary code changes.
Good pattern: multi-stage builds
Multi-stage builds let an image use one temporary “build” stage full of compilers and build tools, then copy only the final compiled artifact into a clean final stage. Because each stage produces its own independent layer stack, the bulky build-tool layers are never part of the final image’s lowerdir chain at all — they simply never get referenced.
Problem
Building an image from a directory that contains large, irrelevant files — local dependency caches, log output, version control history, editor artifacts — without excluding them from the build context.
Why It’s Harmful
Anything copied into an image via COPY or ADD becomes permanent content inside a layer’s diff folder, bloating every downstream pull, every layer-sharing calculation, and every container’s effective lowerdir stack, even though none of it is needed at runtime.
Correct Approach
Maintain an explicit ignore file for the build context so irrelevant paths never reach the build in the first place, keeping each layer’s diff limited to only what the running application actually needs.
Good pattern: squashing intentionally, not by default
Some teams reach for image “squashing” — collapsing an entire layer stack into one — as a blanket size-reduction technique. It’s worth applying deliberately rather than reflexively: squashing throws away the very layer-sharing benefit this whole tutorial has been explaining, since a squashed image can no longer share individual layers with any other image at all. It’s a reasonable trade for a final, rarely-rebuilt distribution artifact, but a poor default for images that are rebuilt frequently or that share a common base with many siblings.
12Best Practices & Common Mistakes
A short, practical checklist distilled from everything covered so far.
Use volumes for write-heavy data
Databases, caches, and log directories should live in a named volume or bind mount, bypassing overlay2’s copy-up path entirely.
Order Dockerfile instructions by change frequency
Rarely-changing instructions first, frequently-changing ones last, to maximize layer cache hits on rebuilds.
Prune regularly
Periodically remove unused images, stopped containers, and dangling layers so orphaned overlay2 directories don’t silently accumulate.
Massive single layers
One giant RUN instruction that installs everything at once is efficient for size, but makes rebuild caching and debugging individual steps harder — balance layer count against layer granularity deliberately.
Writing large files inside the container layer
Modifying large in-layer files repeatedly triggers expensive whole-file copy-ups; redirect that I/O to a volume instead.
Assuming “deleted” means “gone”
Whiteouts hide files from the merged view but never remove earlier bytes from the image — plan secret handling accordingly.
Favor multi-stage builds for compiled languages
Keep build toolchains entirely out of the final image’s layer chain rather than installing and later “removing” them in the same stage.
Manually editing /var/lib/docker/overlay2/
Always go through Docker’s own commands for removing images, containers, and layers so the on-disk state and Docker’s internal metadata database stay in sync.
13Real-World & Industry Examples
overlay2 isn’t confined to Docker Desktop toy examples — it’s the storage foundation under an enormous share of production container infrastructure.
Default across modern Linux distributions
Ubuntu, Debian, Fedora, and most current RHEL/CentOS derivatives configure overlay2 (or the equivalent overlayfs snapshotter) as the out-of-the-box storage backend for both Docker and containerd.
Kubernetes node runtimes
containerd, the container runtime underneath most managed Kubernetes offerings, uses an OverlayFS-based snapshotter as its default image storage mechanism — the same union-filesystem principles from this tutorial, applied at cluster scale across thousands of nodes.
CI/CD pipelines
Continuous integration platforms that spin up short-lived build containers rely heavily on overlay2’s fast, copy-free container start time — a pipeline that launches hundreds of ephemeral containers per hour would be far slower on a storage driver that had to physically duplicate filesystem data for each one.
Rootless container platforms
Podman and rootless Docker installations commonly rely on fuse-overlayfs, the userspace reimplementation of the very same lowerdir/upperdir model, to deliver union-filesystem behavior to unprivileged users who can’t perform a real kernel mount.
Managed cloud container services
Major managed container and serverless-container offerings that run standard Linux container images underneath the hood inherit the same overlay2/OverlayFS-based layer sharing at the node level, which is part of how these platforms keep per-instance cold-start times low even when many customer workloads share the same underlying compute fleet.
Local developer workflows
Even outside production, the everyday developer experience of running docker build dozens of times a day and having most layers “just be there instantly” on rebuild is a direct, visible product of overlay2’s content-addressable, shared-layer design — it’s arguably the single design decision developers interact with most, even if they never see the term “overlay2” itself.
14Redirects, Metadata Copy-Up & the Index Feature
Copy-up and whiteouts explain most of what overlay2 does day to day, but two subtler kernel features quietly fix problems the basic model would otherwise have with renamed directories and repeated copy-ups.
The renamed-directory problem
Imagine a directory named /app/old-config exists down in a lower layer, and a container renames it to /app/new-config. A naive union filesystem would be stuck: it can’t move the directory in the read-only lowerdir, and simply creating a new upperdir directory called new-config wouldn’t automatically know to “inherit” the old directory’s hidden lower-layer contents once merged. OverlayFS solves this with a feature called redirect_dir.
It’s like a library changing a book’s shelf location. Instead of physically moving every page, the librarian just updates the card catalog with a note: “this title now lives at shelf B-12, but see shelf A-4 for the original copy.” A reader following the catalog entry is seamlessly redirected — the actual pages never needed to move.
When a rename like this happens, overlay2 creates the new directory in upperdir and stores a special extended attribute on it — trusted.overlay.redirect — pointing back to the original path down in the lowerdir stack. When the kernel builds the merged view and encounters that attribute, it knows to keep resolving that directory’s original lower-layer contents under the new name, instead of treating it as an empty new folder.
Metadata-only copy-up
Recall from the earlier chapter that copy-up duplicates an entire file’s content into upperdir the moment any part of it is modified. Newer kernels soften this for a common special case: changes that only touch a file’s metadata — its permissions, ownership, or timestamps — rather than its actual byte content. In that situation, the kernel can perform a lightweight “metadata-only” copy-up, recording just the changed attributes without duplicating the full file data, and only falling back to a full data copy-up once the file’s content itself is actually written to.
A very common container action — running chown or chmod across a directory tree at startup to fix file ownership for a non-root user — used to force a full copy-up of every single file it touched, even ones whose content never changed. Metadata-only copy-up makes this class of startup operation dramatically cheaper on large trees.
The index feature and hard-link tracking
OverlayFS also maintains an internal index directory that tracks the relationship between an upperdir copy of a file and its original lower-layer counterpart, primarily to correctly support hard links and to detect certain classes of inconsistency across a layer stack safely. This index is entirely internal bookkeeping — you’ll see its footprint on disk, but Docker and application code never interact with it directly.
redirect_dir
Lets a renamed or moved directory still resolve its original contents from lower layers correctly.
Metadata-only copy-up
Avoids a full file-content copy when only ownership, permission, or timestamp metadata changes.
Index
Internal tracking structure supporting correct hard-link behavior and layer-stack consistency checks.
Kernel-version dependent
These refinements arrived incrementally across kernel releases after the original overlay2 merge, so their exact behavior can vary slightly between older and newer hosts.
15Page Cache, Memory & I/O Performance
Because overlay2 is implemented as a genuine kernel filesystem rather than a block-device abstraction, it interacts with Linux memory management in ways that are worth understanding on their own.
Shared page cache across containers
When two different containers both read the same unmodified file from a shared lower layer — say, the same shared C library from an identical base-image layer — the Linux kernel’s page cache can serve both reads from the very same physical memory pages. The file is only read from disk once; every subsequent container reading that same lower-layer file benefits from the page already being cached in RAM.
Picture a popular reference book kept on a librarian’s front desk instead of the shelves, because so many people ask for it. The first request fetches it from the stacks; every request after that is served instantly from the desk, no matter how many different patrons ask — as long as nobody has “written in” their own personal copy.
Where sharing stops: after copy-up
The moment copy-up duplicates a file into a container’s upperdir, that container now has its own private copy — and its own private page-cache entry for it going forward. Ten containers that have each independently modified the same originally-shared configuration file no longer share any caching benefit for that file; each container’s writes and subsequent reads are now isolated to its own upperdir copy.
Memory accounting and cgroups
Page-cache pages backing a container’s files are generally accounted against that container’s memory cgroup when the container is what caused them to be read or written, which is part of why an image with very large files, or a container that reads a lot of data, can appear to consume more of its memory limit than its actual process heap would suggest — the kernel is counting cached file pages, not just active application memory.
Assuming a container’s memory usage graph only reflects “real” application memory. A container that streams through a large log file or dataset can show a memory usage spike from page cache alone, even though the application’s actual working set is small — this is normal kernel behavior, not a leak, but it regularly confuses teams new to container memory metrics.
Why overlay2 generally outperforms block-based drivers for read-heavy workloads
Because reads pass through the normal Linux filesystem and page-cache path, applications doing mostly reads against image content — a typical web server serving static assets baked into the image, for instance — tend to perform close to native filesystem speed. Block-based drivers like devicemapper, by contrast, introduce an extra layer of block-device indirection for every I/O operation, which is one of the reasons the ecosystem broadly moved toward overlay2 as those older drivers matured out of favor.
16Frequently Asked Questions
No. overlay2 is a Docker storage driver that sits on top of an already-formatted filesystem (typically ext4 or xfs) and uses the kernel’s OverlayFS feature to stack directories from that underlying filesystem.
Because shared base layers between images are stored exactly once on disk. The “size” reported per image includes shared layers in its total, so the numbers overlap rather than add up.
No. A whiteout only hides the file from the merged view of a running container; the original bytes remain in whichever lower (image) layer they were created in.
Most likely the database’s data directory is sitting on overlay2’s writable layer, so every modified page inside its data files triggers a whole-file copy-up the first time it’s touched. Mounting the data directory as a volume removes this overhead entirely.
Docker uses exactly one storage driver per daemon at a time; changing it typically requires re-pulling images and rebuilding containers, since the on-disk layout is driver-specific.
Yes — for any reasonably modern Linux kernel and filesystem combination, overlay2 remains the officially recommended and most widely used storage driver.
Generally no — OverlayFS has historically had limited or unreliable support for network filesystems like NFS as a lower or upper layer, so /var/lib/docker is expected to sit on a genuine local filesystem such as ext4 or xfs.
It can. Stacking overlay2 on top of overlay2 (an overlay filesystem as the lowerdir for another overlay mount) has historically been unsupported or restricted by the kernel, which is why Docker-in-Docker setups often fall back to a different storage strategy inside the nested daemon.
docker inspect show a different hash than the folder name under overlay2/?The folder name is a local, host-specific cache-id, while docker inspect and docker images typically display content-addressable digests (the layer’s DiffID or the distributable digest). Docker keeps an internal mapping between the two; they’re deliberately different identifiers for different purposes.
/var/lib/docker/overlay2/ to free space?Not directly — Docker’s internal metadata database tracks which layers belong to which images and containers, and manually deleting a folder it still references can corrupt that bookkeeping. Use Docker’s own pruning commands, which safely remove only genuinely unreferenced layers.
17Summary and Key Takeaways
overlay2 turns the abstract idea of a “container image” into something concrete and efficient: read-only stacked layers on disk, merged live by the kernel, with a thin writable layer capturing whatever a specific container changes. Nothing is copied until it has to be, nothing is truly deleted until the layer that created it is gone, and the entire system is built on two elegantly simple tricks — copy-up and whiteouts — repeated consistently at every level, from a single file edit to an entire multi-gigabyte image.
Key Takeaways
- overlay2 is a Docker storage driver — it wraps the Linux kernel’s OverlayFS feature; it isn’t a filesystem you format a disk with.
- Four directory roles matter — lowerdir (read-only image layers), upperdir (container writes), merged (unified view), workdir (kernel scratch space).
- Copy-up powers “editing” read-only files — the whole file is copied into upperdir before any change is applied, which is expensive for large files.
- Whiteout markers fake deletion — the original data in a lower layer is never actually removed, only hidden from the merged view.
- Shared layers are the efficiency secret — identical content-hashed layers are stored once on disk no matter how many images or containers reference them.
- Every container’s writable data disappears on removal — unless it was placed in a volume or bind mount outside the overlay2 stack.
- Dockerfile authoring choices directly shape overlay2 behavior — instruction ordering and grouping control both build-cache efficiency and final image size.