Chapter 2: Images
Topic 11

Inspecting an Image

InspectSupply chain

An image is a dependency you are about to trust with your application, and it can be read like one. docker image inspect, docker history, and a glance at its layers and size tell you what base it's built on, what command it runs, what ports it exposes, and — the part most people skip — whether anything sensitive was baked into its history. Reading an image before you run it is a basic supply-chain habit, no different from skimming a library's source before you import it.

Driftwood depends on postgres:16 and nginx:1.27-alpine, and this topic reads both the way you'd read a third-party package: what they execute by default, what they expose, what they declare as volumes, and whether their build hid anything you'd rather know about.

docker image inspect

docker image inspect dumps the image config as JSON: the default CMD and ENTRYPOINT, the ENV variables, exposed ports, the working directory, declared volumes, labels, and the architecture. Reading it tells you what a container will do before you start one — what process runs, what environment it inherits, what it expects to listen on.

docker image inspect postgres:16 — the config tells you what a container will do
$ docker image inspect postgres:16 --format '{{json .Config}}' | jq
{
  "Entrypoint": ["docker-entrypoint.sh"],
  "Cmd": ["postgres"],
  "Env": ["PG_MAJOR=16", "PGDATA=/var/lib/postgresql/data", "..."],
  "ExposedPorts": { "5432/tcp": {} },
  "Volumes": { "/var/lib/postgresql/data": {} },
  "WorkingDir": "",
  "Labels": null
}

For postgres:16 the readout is concrete: it runs docker-entrypoint.sh postgres, exposes 5432/tcp, and declares /var/lib/postgresql/data as a volume. That last line matters — it means an anonymous volume appears automatically unless you mount your own, which is exactly the kind of surprise you want to know about before wiring the image into Compose.

docker history

docker history lists each layer with the Dockerfile instruction that created it and its size. It is how the image was built, read backwards — and it is where secrets hide. A value passed via ARG or ENV, or a file added in one layer and "deleted" in a later one, shows up in the history even though it isn't visible in a running container.

docker history nginx:1.27-alpine — each layer, its instruction, and its size
$ docker history nginx:1.27-alpine
IMAGE          CREATED       CREATED BY                                      SIZE
b4e3f1a2c9d0   2 weeks ago   CMD ["nginx" "-g" "daemon off;"]                0B
<missing>      2 weeks ago   EXPOSE 80                                       0B
<missing>      2 weeks ago   RUN /bin/sh -c set -x && apk add --no-cache …   24.1MB
<missing>      2 weeks ago   COPY docker-entrypoint.sh /                     2.6KB
<missing>      3 weeks ago   /bin/sh -c #(nop) ADD file:… in /               7.4MB

The per-layer instructions read like a condensed Dockerfile. If a build had done RUN echo $SECRET > /tmp/key && … && rm /tmp/key, the key's bytes would still be in that layer regardless of the later rm — which is precisely why build secrets need BuildKit's --secret mount rather than an ARG, a point the Dockerfile chapter develops.

Two readouts, two questions they answer
docker image inspect
The config: default CMD/ENTRYPOINT, ENV, exposed ports, volumes. Tells you what a container will do before you start one.
docker history
The per-layer build instructions and their sizes. How it was built, read backwards — and where baked-in secrets and bloated layers show up.

Reading Size and Layers

Per-layer sizes show where the weight actually is. An image's total is just the sum of its layers, so a single 400 MB layer is usually one uncleaned package cache or one fat COPY — and docker history names the exact instruction that produced it. That turns image-shrinking from guesswork into targeting: you fix the instruction that costs the bytes, not whichever one you noticed first.

This is the diagnostic that directs the size work in the Dockerfile chapter. Before optimizing anything, you read the history, find the heaviest layer, and look at the instruction above it — the bloat almost always has a single, nameable cause.

Labels and Provenance

Well-built images carry OCI labels — source repository, revision, build date — recorded in the config. Their presence is a quick signal of how carefully an image was produced: an image that declares where its source lives and which commit built it is one you can trace, while one with Labels: null tells you nothing about its origin. It's a thirty-second provenance check before you depend on something.

Inspecting Driftwood's Dependencies

Reading postgres:16 and nginx:1.27-alpine this way surfaces exactly what you need before composing them. Postgres runs postgres under its entrypoint, listens on 5432, and declares a data volume; nginx runs nginx -g 'daemon off;', exposes 80, and copies in an entrypoint script. Those are the defaults, ports, and volumes you'll wire into Compose later — and knowing them now means no surprises about anonymous volumes or unreachable services then.

The habit generalizes past these two. Any image you're about to run, you inspect first: its entrypoint so you know what executes, its history so you know what's hidden, its exposed ports and volumes so you know how it'll behave. It costs a minute and prevents a class of "why is this doing that" debugging later.

Common Mistakes
  • Running an unfamiliar image without inspecting its ENTRYPOINT/CMD, env, and history — you don't know what it executes, what it exposes, or what's hidden in its layers until something surprises you.
  • Passing a secret via --build-arg and assuming it's gone from the image — docker history and the layers still hold it; build secrets need BuildKit's --secret mount, covered in the Dockerfile chapter.
  • Treating image size as a black box rather than reading per-layer sizes — without docker history you optimize blind instead of fixing the one instruction that produced the bloated layer.
  • Ignoring whether an image declares volumes or exposes ports before composing it — then being surprised by an anonymous volume or an unreachable service that the config would have warned you about.
Best Practices
  • Inspect any third-party image's config and history before depending on it, the same way you'd skim a library's source before importing it.
  • Use docker history to audit your own images for accidentally baked-in secrets or oversized layers before pushing them to a registry.
  • Read per-layer sizes to aim image-shrinking work at the instruction that actually costs the bytes, rather than guessing.
  • Prefer images that carry provenance labels (source, revision) and add them to your own builds so every image is traceable to the commit that produced it.
Comparable tools skopeo inspect reads image config and manifests without pulling dive a dedicated layer and size explorer syft · trivy SBOM generation and vulnerability scanning that go deeper into contents

Knowledge Check

What does docker image inspect reveal that docker history does not?

  • The image's config — default command, env, exposed ports, and volumes — whereas history shows the build instructions
  • The live, continuously streaming logs of every single container that happens to be running from this particular image right now
  • A direct way to rewrite the entrypoint and default command of the image in place without a rebuild
  • Whether you are currently authenticated to the remote registry that stores and serves this image

Why does a secret passed via ARG or ENV survive in an image even after a later layer "deletes" it?

  • It is recorded in the build history and the layer that added it, and a later deletion only masks it rather than removing the bytes
  • ENV values are encrypted at build time, but the matching decryption key is shipped right alongside them in the very same image, so they decrypt trivially
  • The daemon silently re-injects the secret back into the container at run time from an internal cache it never clears
  • Rebuilding the image from the Dockerfile is the only operation that re-adds the secret, so its presence cannot be avoided

How do per-layer sizes help you shrink an image?

  • They locate the heaviest layer and the instruction that produced it, so you fix the bloat at its source
  • Simply reading the per-layer sizes automatically compresses the single largest layer in order to reclaim disk space
  • They let the daemon selectively skip loading the largest layers at container start in order to save host memory
  • They reveal exactly which tag the image was originally pushed under, so you can re-tag it under a smaller one

What should you check on a third-party image before running it?

  • Its entrypoint and command, env, exposed ports, declared volumes, and history for anything hidden in its layers
  • Only its total download and pull count and star rating on Docker Hub, which on its own conclusively confirms the image is entirely safe to run
  • Only how recently its tag was last pushed, which on its own guarantees the image has no secrets baked in
  • Only whether its Docker Hub listing page has a clean, well-formatted readme and polished documentation

You got correct