← Back to Docker Base Image Pin Check

Docker Base Image Pin Check vs. reading a Dockerfile by hand

Skimming a Dockerfile for an obvious FROM node or FROM node:latest genuinely works, and it is the method most people already use. This page names no product or company being compared against: "reading the Dockerfile by hand" is a generic manual habit, not a competing tool.

CaseReading the Dockerfile by handThis tool
Bare FROM node or explicit FROM node:latestCorrectly caught by a reviewer who knows to look for itCorrectly caught the same way
A specific-looking tag, FROM node:18Reads as already careful -- easy to not flag, even though the tag can still be repointedClassifies any non-digest tag as mutable, specific-looking or not
A later FROM builder referencing an earlier AS builder stageRequires remembering every stage alias defined earlier in a long file to avoid a false alarmTracks every AS <name> alias while scanning and checks later FROM lines against that running set automatically
A private registry reference with a port, myregistry.local:5000/node:18Easy to misread the port's colon as the tag delimiterAlways locates the tag inside the last /-separated path segment, never the host
Multiple Dockerfiles across a repository (Dockerfile, Dockerfile.prod, worker.dockerfile)Coverage depends on remembering every file existsCLI discovers every matching filename under a directory on its own

The gap that matters most: the specific-looking tag

node:18 is the trap this tool exists to catch. It reads as more careful than bare node or node:latest, and it is not pinned in the sense that matters -- Docker still resolves 18 to whatever bytes that tag points to right now, and unlike a version bump you would review, a later push to the same tag is silent. A reviewer scanning for "obviously risky" names has no reason to single this out. This tool draws no such distinction: anything short of a content digest classifies as mutable, regardless of how specific the tag looks.

The gap that matters second: telling a build stage from a registry pull

A multi-stage Dockerfile reuses names for its own internal stages, and a later FROM <name> that refers to one of them is not a registry pull at all -- it has no tag to pin. Getting this right by eye means holding every AS <name> alias defined earlier in the file in mind while reading each subsequent FROM line, which gets harder as a Dockerfile grows more stages. This tool tracks the alias set mechanically as it scans, so the distinction does not depend on what a reader remembered from forty lines up.

When to still read the Dockerfile yourself

This tool answers one question -- is this FROM line pinned to a content digest -- and stops there. It does not resolve a mutable tag to a digest for you (it prints the docker buildx imagetools inspect command instead), and it makes no claim about whether the code inside a pinned image is itself safe. Deciding whether to trust a given digest, and reviewing what a base image actually contains, is still a job for you.

Common questions

What does reading a Dockerfile by hand actually catch?

A reviewer who knows to look for it can spot the obvious case immediately: FROM node with no tag at all, or an explicit FROM node:latest. Both read as risky on sight, because latest is a well-known name for "whatever the publisher pushed most recently." That review genuinely works for the obvious case -- the same case this tool's classify.mjs also flags.

Why does a specific-looking tag like node:18 slip past a skim?

node:18 reads as more careful than bare node or node:latest, because it names a specific major version -- but Docker still resolves :18 to whatever bytes that tag currently points to, and the publisher can push a new build to it at any time with no version bump to review. A reviewer scanning for the word "latest" or a missing tag has no reason to flag node:18, since nothing about it looks wrong. This tool draws no such distinction: every tag, specific-looking or not, that isn't a content digest classifies as mutable.

Why is telling a build-stage reference apart from a registry pull easy to get wrong by eye?

A multi-stage Dockerfile can define FROM golang:1.22 AS builder early on and then write FROM builder forty lines later to continue that stage -- and FROM builder is not a registry pull at all, so it has no tag to pin and nothing to flag. A reviewer working through a long Dockerfile has to remember every stage alias defined earlier in the file to make that call correctly; forgetting one risks either a false alarm on a name that is not a registry image, or missing that a later re-use of the same word is a genuine external FROM. This tool tracks every AS <name> alias as it scans and checks each later FROM against that running set automatically, so the distinction never depends on what a reader kept in their head.

Why is a registry with a port number easy to misread?

A reference like myregistry.local:5000/node:18 contains two colons, and naively splitting on the first one reads :5000/node as the tag -- nonsense that hides the real tag, 18. The actual grammar is that a colon only starts a tag inside the last slash-separated path segment; the colon in registry:5000/ is part of the host, not a tag delimiter. Getting this right by eye on an unfamiliar private-registry reference is easy to get backwards. This tool's splitNameAndRef always locates the tag inside the last path segment only, so a registry port is never misread as one.

Does this replace checking every Dockerfile in a repository?

No, but it removes the tedium of doing so by hand. A repository can carry a Dockerfile, a Dockerfile.prod, and a worker.dockerfile, each with its own FROM lines, and a manual review has to remember to open every one, every time a new file is added. The CLI (python3 check.py) discovers Dockerfile, Dockerfile.*, and *.dockerfile anywhere under a given directory on its own, so coverage does not depend on a reviewer's memory of which files exist.