Skip to content

Image History


Back in the inspecting-images lesson, you met docker image history for the first time — enough to know it lists layers and shows what created each one. That was the introduction. This lesson is where it actually becomes useful.

Right now, if someone handed you an image and asked “why is this 900MB when it should be 200MB,” or “did my caching actually work,” or “what exactly changed between these two versions” — could you answer with confidence, just from the command line? By the end of this lesson, yes. We’re going to slow down on the one command you already know and squeeze everything out of it.

    flowchart LR
    A[docker image history] --> B[Every layer, in order]
    B --> C[What created it]
    B --> D[How big it is]
    C --> E["Bloat hunting"]
    D --> E
  

A Quick Recap

The command hasn’t changed:

docker image history mysite:v1
IMAGE          CREATED         CREATED BY                                SIZE
7a1b2c3d4e5f   3 seconds ago   CMD ["nginx" "-g" "daemon off;"]          0B
<missing>      3 seconds ago   COPY index.html .                        612B
<missing>      3 seconds ago   RUN apt-get install -y nginx              68MB
<missing>      3 seconds ago   WORKDIR /var/www/html                     0B
<missing>      3 seconds ago   FROM ubuntu:22.04                        77MB

Every row is one instruction from the Dockerfile you wrote, read from newest at the top to oldest at the bottom. What we skipped past last time is why some columns look the way they do, and how to actually use this table to make decisions.

Why So Many <missing> Entries?

Only the very last layer of an image gets a real, addressable ID — the one you see in docker image ls. Every layer before it exists, and takes up real disk space, but it doesn’t get its own top-level image ID of its own. docker image history shows those as <missing> — not because anything is broken or lost, but because they were never meant to be referenced on their own. They’re intermediate steps, not standalone images.

This matters because it tells you something important: you can’t docker image rm an individual layer. Layers are only ever removed as a side effect of removing every image that depends on them. Cleanup, from the earlier lesson, always operates on whole images — never on a single row of this table.

Seeing the Full Command, Untruncated

By default, the CREATED BY column cuts long commands short with ...:

RUN apt-get update && apt-get install -y nginx cur...

For a quick skim that’s fine. For actually debugging, it’s not enough — you need the whole thing:

docker image history --no-trunc mysite:v1
IMAGE          CREATED BY
7a1b2c3d4e5f   CMD ["nginx" "-g" "daemon off;"]
<missing>      COPY index.html . # buildkit
<missing>      RUN /bin/sh -c apt-get update && apt-get install -y nginx
<missing>      WORKDIR /var/www/html
<missing>      FROM ubuntu:22.04

--no-trunc is the flag you reach for the moment docker image history alone leaves you guessing. Keep it in your back pocket rather than memorizing it up front — you’ll rediscover the need for it naturally the first time a RUN line gets cut off mid-command.

Formatting for Readability

Just like docker image ls, history accepts --format with Go templates, and a couple of extra flags worth knowing:

docker image history --format "table {{.CreatedBy}}\t{{.Size}}" mysite:v1
CREATED BY                                 SIZE
CMD ["nginx" "-g" "daemon off;"]          0B
COPY index.html .                          612B
RUN apt-get install -y nginx               68MB
WORKDIR /var/www/html                      0B
FROM ubuntu:22.04                          77MB

Sizes shown this way are already human-readable (68MB, not bytes) by default — if you ever see raw byte counts in a script’s output, --human (on by default in normal use, but explicit when scripting) is what controls that.

Hunting for Bloat, for Real

This is where history earns its keep. Say you update (see the RUN line) the Dockerfile from the earlier lesson and, out of habit, forget to clean up after installing:

FROM ubuntu:22.04
WORKDIR /var/www/html
RUN apt-get update && apt-get install -y nginx curl vim
COPY index.html .
CMD ["nginx", "-g", "daemon off;"]

Build it, then look at the history:

IMAGE          CREATED BY                                          SIZE
7a1b2c3d4e5f   CMD [...]                                            0B
<missing>      COPY index.html .                                   612B
<missing>      RUN apt-get install -y nginx curl vim                210MB
<missing>      WORKDIR /var/www/html                                0B
<missing>      FROM ubuntu:22.04                                    77MB

That one RUN line jumped from 68MB to 210MB the moment vim and its dependencies got pulled in — and history is exactly how you’d catch that, rather than just noticing “the image feels bigger” and guessing why. This is the habit worth building: whenever an image’s total size surprises you, docker image history is the first place to look, not the last.

Note

history tells you which layer is heavy — it doesn’t tell you why on its own. Once you’ve found the offending RUN line, the fix usually lives in the Dockerfile itself (install only what you need, clean up caches in the same layer), which we’ll get into properly once we cover best practices.

Confirming Your Caching Actually Worked

You saw CACHED in the build output back in the previous lesson, but history gives you a second, independent way to confirm layer reuse actually happened — useful when you’re looking at an image someone else built and have no build log to check.

Compare the CREATED timestamps across rows:

IMAGE          CREATED         CREATED BY
7a1b2c3d4e5f   2 minutes ago   CMD [...]
<missing>      2 minutes ago   COPY index.html .
<missing>      3 days ago      RUN apt-get install -y nginx
<missing>      3 days ago      WORKDIR /var/www/html
<missing>      3 days ago      FROM ubuntu:22.04

A gap like this — old timestamps at the bottom, fresh ones only at the top — is the fingerprint of a cache doing its job. Only the layers below your changed line got reused from three days ago; only what came after actually got rebuilt just now. If instead every timestamp is identical and recent, the whole image was rebuilt from scratch, which is worth noticing since it might mean your instruction ordering isn’t protecting the cache the way you intended.

Comparing Two Versions Side by Side

When you bump a Dockerfile and want to know exactly what changed layer-for-layer, run history on both tags and diff the output:

diff <(docker image history --no-trunc myapp:v1) <(docker image history --no-trunc myapp:v2)
5c5
< <missing>   RUN apt-get install -y nginx
---
> <missing>   RUN apt-get install -y nginx curl

This is a more targeted version of the full docker image inspect diff from the earlier lesson — inspect compares the final configuration, history compares the steps that produced it. When you specifically want to know “what did the Dockerfile do differently,” history is the sharper tool of the two.

History vs. Inspect: Picking the Right One

Both commands come up constantly, and it’s worth being explicit about when each one is the right call:

Question you’re askingCommand
What’s the final environment, ports, entrypoint?docker image inspect
Why is this image so big, and where?docker image history
What exact steps built this image?docker image history --no-trunc
Did my dependency-caching order actually help?docker image history (timestamps)
What’s the exposed port or working directory right now?docker image inspect

Neither replaces the other — inspect tells you what an image is, history tells you how it got that way.

Wrapping Up

docker image history looked like a simple listing command the first time you saw it, and technically it still is — but now you know how to actually read it: catching bloat layer by layer, confirming caching worked without needing the original build log, and diffing two versions to see exactly what changed. It’s a small command that punches well above its weight once you know what to look for in it.

Last updated on