How does `docker diff` help you find what a running container has written, and where does it mislead you?
answer
- Compared against the image, not against nothing
- Three single-letter prefixes on each line
- Added, Changed, Deleted
- Parent directories inflate the output
- Mounted paths never appear in it
basics
~20 sdocker diff CONTAINER lists every path that differs from the container's image, prefixed A for added, C for changed and D for deleted. It shows no sizes, no file contents, and nothing under a volume or bind mount — so a disk problem in mounted storage is invisible to it.
solid answer
~50 s`docker diff` compares the container's filesystem against the image it was started from and prints one line per changed path: **A** added, **C** changed, **D** deleted. It reads the container's state directly, so it works on a stopped container too, and it is the fastest way to answer "what has this container written that its image did not contain?" — usually a log directory, a package-manager cache or an unexpected upload directory. Three limits matter. It prints **paths only**: no sizes, no timestamps, no contents, so you still need `docker ps -s` for the writable layer's total size and a `du` inside the container to find the big files. It marks every **parent directory** of an added file as `C`, which inflates the output and confuses people. And it covers only the container's own filesystem: anything under a **named volume, bind mount or tmpfs** is excluded, so a container filling a mounted data disk produces a clean, misleadingly short diff.
code
bash · 5 linesdocker diff ingest-gw | grep '^A' | wc -l
docker diff ingest-gw | grep '^A' | cut -d/ -f2-3 | sort | uniq -c | sort -rn | head
docker ps -s --filter name=ingest-gw
docker inspect ingest-gw --format '{{json .Mounts}}'go deeper
Know that docker diff lists paths that differ from the container's image, and what the A, C and D prefixes mean. Do not expect it to print file contents or sizes.
Explain that the comparison is against the image over the container's own filesystem, why parent directories show as changed, and why anything under a volume or bind mount is missing from the output.
Use it as a drift detector during an incident: filter to added paths, spot data being written where a mount should be, and pair it with docker ps -s and du before concluding anything about disk usage.
Own the standard it reveals: containers should be disposable, so persistent state belongs in a declared mount and platform state belongs in the image. Decide how that expectation is checked before an image reaches production, not during an incident.
### What the command reports `docker diff CONTAINER` walks the container's filesystem and compares it with the image it was created from, printing one line per differing path with a single-letter prefix: - `A /var/lib/ingest/spool/0f3a.batch` — **added**: the path exists in the container and not in the image; - `C /etc/ingest/config.yaml` — **changed**: the path exists in both and differs; - `D /usr/share/doc/python3/changelog.gz` — **deleted**: the image had it and the container does not. It needs no process inside the container, so it works on a container that has already exited — which is when you usually want it. There are no flags: the output is the whole comparison, and you filter it with `grep`. ### A worked example A telemetry ingest gateway — a Python FastAPI service on `python:slim` — is the noisy neighbour on a host that is at 94% disk. `docker ps -s` shows that container's writable layer at **4.31 GB**, against a virtual size barely above the image's. So the growth is in the container itself, not in the image, and `docker diff` will see it: ``` docker diff ingest-gw | grep '^A' | wc -l # 3412 added paths docker diff ingest-gw | grep '^A' | cut -d/ -f2-3 | sort | uniq -c | sort -rn | head ``` The histogram points at `/var/lib/ingest/spool`: the gateway's retry spool, which the team believed was on a mounted volume and is not, because the `-v` flag was dropped when the run command was rewritten. Everything the service has spooled for 17 days is sitting in the writable layer, and will be deleted the moment the container is recreated. `docker diff` did not measure a single byte — `docker ps -s` gave the size and `docker exec ingest-gw du -sh /var/lib/ingest/spool` gave the breakdown — but it is what turned "disk is full" into "this directory should have been a mount and isn't". ### The three ways it misleads **1. Mounted paths are invisible.** The comparison is against the image, over the container's own filesystem. A named volume, a bind mount or a tmpfs is mounted *over* a path, and its contents belong to the volume or the host. So the failure mode is exactly inverted from the example above: if the spool directory *were* a volume, the container would be filling the host's disk and `docker diff` would print almost nothing. A short diff is not evidence that a container is writing little; it is evidence that it is not writing into its **writable layer**. Check the mounts (`docker inspect --format '{{json .Mounts}}'`) before drawing any conclusion from a quiet diff. **2. Parent directories look like changes.** Adding one file marks its whole parent chain `C`, so a single new file under a deep path produces several lines. Reading `C /usr` as "something modified /usr" is a beginner's mistake; filter to `^A` and `^D` when you want the real signal. **3. It is a path list, not an accounting.** No sizes, no mtimes, no contents. Ten thousand empty files and one 4 GB core dump look identical in the output. Pair it with `docker ps -s` for the writable-layer total and `du` or `find -size` inside the container for the distribution. One further practical caveat: on a container whose filesystem holds a very large number of files the comparison has to enumerate them, so on a fat image with a busy writable layer the command can take a noticeable while and return a wall of output. Pipe it through `grep`/`sort` from the start rather than staring at it. ### Why this is a senior habit The interesting output is rarely the size — it is the **shape**. Additions under a directory that ought to be immutable mean someone has been `docker exec`ing into production, or the application is treating a container path as durable storage. Deletions under `/usr` or `/etc` in a long-running container mean something modified the platform in place. Both are drift: the running container no longer matches its image, and the next `docker rm` and re-run silently reverts it. `docker diff` is how you see that drift in ten seconds, and the fix is never "clean up the writable layer" — it is a mount for the data that should persist, or a Dockerfile change for the state that should have been in the image.
- `docker diff` on a container that is clearly filling a disk prints only a handful of lines. What does that tell you?That the writes are not going into the container's writable layer — they are going into a named volume, a bind mount or a tmpfs, which the comparison excludes. Read `docker inspect`'s Mounts and look at the host path or volume directory instead; the container may be perfectly clean while the host disk fills.
- Why does `docker diff` show `C` on directories nobody touched?Adding or removing an entry changes the directory itself, so every parent of an added or deleted path is reported as changed. It is an artefact of how the comparison works, not evidence of modification. Filter to `^A` and `^D` lines when you want the actual changes.
- How do you turn a `docker diff` path list into an answer about disk usage?`docker diff` reports no sizes. Get the writable layer's total from `docker ps -s`, then find the distribution with `docker exec CONTAINER du -sh /path/*` or a `find` by size. Use the diff to narrow the candidate paths and the other two to measure them.
saying these in an interview costs you the question
- Reads C on a parent directory as a real modification
- Believes `docker diff` reports file sizes
- Assumes an empty diff means the container writes nothing
- Thinks it compares two containers or two images
- Expects volume and bind-mount contents to appear
- Says the container must be running for it to work