Skip to main content
RunBook Academy

Docker & ContainersII · Linux InternalsOverlayFS

OverlayFS — the filesystem underneath every container

Intermediate⏱ ~22 min

What you'll learn

  • Explain the upper/lower/work/merged OverlayFS model
  • Mount an OverlayFS manually with mount(8)
  • Recognise copy-up semantics and their performance implications

Prerequisites

None — start here.

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-09

Not yet marked complete on this device.

A container’s root filesystem looks like a normal Linux tree. It isn’t. It is an OverlayFS composition of multiple read-only image layers plus one read-write upper layer that captures every change.

Understanding OverlayFS is the difference between “my container is 1.2 GB” being an accurate size and a misleading one. And between “why is apt-get install slow the second time” being a mystery and a known consequence of copy-up.

The four directories

flowchart TB
  L1["Lower (image layer 1)"] --> M[Merged]
  L2["Lower (image layer 2)"] --> M
  L3["Lower (image layer 3 - Ubuntu)"] --> M
  W[Work] --> M
  U["Upper (container read-write)"] --> M
  W -.-> U

OverlayFS combines three (or more) directories into one visible filesystem:

  • lower — read-only image layers, stacked. The lowest is the bottom of the image; the highest is closest to the container’s root.
  • upper — read-write. Every change the container makes lands here. This is what docker diff reports.
  • work — internal scratch directory OverlayFS uses during copy-up. Must be on the same filesystem as upper.
  • merged — the resulting view. What the container sees.

The visible filesystem is the merged view. From inside the container, the layered nature is invisible. From the host, it is visible via /var/lib/docker/overlay2/<id>/.

Mounting OverlayFS manually

This is the single most useful debugging tool for understanding containers. You can do everything OverlayFS does without Docker.

Destructivemanual overlay mount
mkdir -p /tmp/lower1 /tmp/lower2 /tmp/upper /tmp/work /tmp/merged
echo 'from lower1' > /tmp/lower1/file.txt
echo 'from lower2' > /tmp/lower2/file.txt
mount -t overlay overlay -o lowerdir=/tmp/lower2:/tmp/lower1,upperdir=/tmp/upper,workdir=/tmp/work /tmp/merged
cat /tmp/merged/file.txt
echo 'new content' > /tmp/merged/file.txt
cat /tmp/upper/file.txt
umount /tmp/merged
Read-only / Safeobserve copy-up
$ mkdir -p /tmp/lower1 /tmp/lower2 /tmp/upper /tmp/work /tmp/merged
echo 'from lower1' > /tmp/lower1/file.txt
echo 'from lower2' > /tmp/lower2/file.txt
mount -t overlay overlay -o lowerdir=/tmp/lower2:/tmp/lower1,upperdir=/tmp/upper,workdir=/tmp/work /tmp/merged
echo '--- merged view ---'
cat /tmp/merged/file.txt
echo '--- writing through merged ---'
echo 'overwritten' > /tmp/merged/file.txt
echo '--- upper dir after write ---'
ls /tmp/upper/
cat /tmp/upper/file.txt
echo '--- lower1 unchanged ---'
cat /tmp/lower1/file.txt
umount /tmp/merged
--- merged view ---
from lower2
--- writing through merged ---
--- upper dir after write ---
file.txt
overwritten
--- lower1 unchanged ---
from lower1

What this means for Docker images

A Docker image is a list of layers. Each FROM, RUN, COPY, or ADD instruction produces a new layer. When a container starts:

  • All image layers become lower directories.
  • A new empty upper directory is created.
  • A work directory is created.
  • The merged view is mounted at the container’s root.

When the container deletes a file that exists in a lower layer, the deletion is recorded as a “whiteout” file in the upper layer. The file still exists in the lower; it’s just invisible in the merged view.

Read-only / Safewhat has been copied up, and what did it cost?
CONTAINER=web
docker diff "$CONTAINER" | head -20
echo '--- size of the writable layer ---'
docker ps --size --filter "name=$CONTAINER" --format '{{.Names}}\t{{.Size}}'
C /usr
C /usr/local
A /usr/local/etc
A /usr/local/etc/redis
A /usr/local/etc/redis/redis.conf
web	4.02GB (virtual 4.31GB)

Illustrative output

Read the prefixes: A is a file that only ever existed in the upper layer, which costs exactly what it contains. C is a copy-up, which costs the size of the original. D is a whiteout, which costs nothing but does not reclaim anything either. The docker ps --size figure is the writable layer alone — the number before “virtual” — and it is the one that should be near zero on a well-behaved container.

The three fixes, in the order you should reach for them:

  1. Put mutable data in a volume. A volume is a bind of a host directory into the container; it is not part of the overlay, so writes go straight to the filesystem with no copy-up at any size. This is the answer for databases, indexes and anything the application opens read-write.
  2. Do the mutation at build time. If the file only needs its header rewritten once, rewrite it in the Dockerfile so the layer ships in its final state. The copy then happens once during the build instead of once per container per start.
  3. **Do not chown -R image content at runtime.** Set ownership in the Dockerfile with COPY --chown=, which bakes the right UID into the layer and removes the reason for the recursive copy entirely.

Inspecting a running container’s layers from the host

Read-only / Safefind the container's overlay directories
CONTAINER=web
docker inspect --format '{{json .GraphDriver}}' "$CONTAINER" | jq .
UPPER=$(docker inspect --format '{{.GraphDriver.Data.UpperDir}}' "$CONTAINER")
echo "upper: $UPPER"
sudo du -sh "$UPPER"
{
"Data": {
  "LowerDir": "/var/lib/docker/overlay2/2f9c.../diff:/var/lib/docker/overlay2/8ab1.../diff",
  "MergedDir": "/var/lib/docker/overlay2/61de.../merged",
  "UpperDir": "/var/lib/docker/overlay2/61de.../diff",
  "WorkDir": "/var/lib/docker/overlay2/61de.../work"
},
"Name": "overlay2"
}
upper: /var/lib/docker/overlay2/61de.../diff
4.0G	/var/lib/docker/overlay2/61de.../diff

Illustrative output

Read-only / Safethe largest files in the writable layer
CONTAINER=web
UPPER=$(docker inspect --format '{{.GraphDriver.Data.UpperDir}}' "$CONTAINER")
sudo find "$UPPER" -type f -printf '%s\t%p\n' 2>/dev/null | sort -rn | head -10 | numfmt --field=1 --to=iec
4.0G	/var/lib/docker/overlay2/61de.../diff/opt/model/index.db
12M	/var/lib/docker/overlay2/61de.../diff/var/log/app/current
4.0K	/var/lib/docker/overlay2/61de.../diff/etc/app/config.yaml

Illustrative output

A 4 GB file at the top of that list, with the same path present in the image, is a copy-up and not a leak. docker image inspect the base image and look for the same path: if it is there, no amount of log rotation will help, and the file belongs in a volume.

Read-only / Safeimage size
docker image inspect nginx --format '{{.Size}}' | numfmt --to=iec
docker image history nginx --no-trunc --format '{{.CreatedBy}}  {{.Size}}' | head -10

Performance characteristics

  • Reads of unchanged files: one extra indirection (look up in upper, fall through to lower). Negligible cost.
  • Writes to unchanged files: copy-up from lower to upper, then write. This is the cost most operators underestimate.
  • Writes to already-copied files: write to upper only. Cheap.
  • Deletes of files in lower: whiteout file in upper. Cheap.
  • Many small writes to a fresh container: every file gets copy-up before the write. This is why apt-get install is slow on first run after pulling a new layer.

For workloads that write a lot of small files, tmpfs (which is not OverlayFS) or a direct bind-mount to a fast filesystem may be faster.

Knowledge check

Knowledge check · 5 questions

  1. Q1. Which OverlayFS directory captures every change a running container makes?

  2. Q2. When a container writes to a file for the first time, OverlayFS modifies the lower layer in place.

  3. Q3. Why does the upper and work directory have to be on the same filesystem?

  4. Q4. A container writes 8 bytes to a 4 GB file that came from the image. How much data does OverlayFS move, and how much does the writable layer grow?

  5. Q5. Which of these trigger a full copy-up of a file that came from an image layer? Select all that apply.

Passing score: 75%. Answers are checked in this browser.