Select display theme

Docker Doctor: a health score for your Dockerfiles

Categories:
DevOps
Developer Tools

July 23, 2026Noppakorn Kaewsalabnil

Docker Doctor is an opinionated static analysis CLI for Dockerfile and Docker Compose files. This post covers the design decisions behind it: the doctor-tool pattern it borrows from React Doctor, how the 0-100 health score is computed, and where static analysis stops being useful.

You push to main. The image builds, CI is green, the container starts, and nobody looks at the Dockerfile again. Six months later it is still running as root, still pulling node:latest, still shipping a gigabyte of build tooling to production. Nothing failed, so nothing got fixed.

I built Docker Doctor because I kept writing that Dockerfile. It is an opinionated static analysis CLI for Dockerfile and Docker Compose files: one command, 21+ rules across security, performance, best practices, Compose, and image size, and a 0–100 health score at the end. Rather than walk through the feature list (the repo covers that), this post is about the design decisions behind it: why a score instead of a wall of errors, why the score decays instead of subtracting, and where a tool like this stops being useful.

The doctor-tool pattern

The shape of the tool is borrowed. React Doctor scans a React codebase and hands you a score; Shad Scan does the same for shadcn/ui projects. What both get right is the onboarding: you don't write a config, you don't pick a preset, you don't negotiate rule severities with your team. You run one command against the project you already have and get a number you can react to.

That matters because the alternative already exists and mostly goes unused. Every team I have worked with has a carefully configured linter for application code, and a Dockerfile that no tool has ever looked at. The Dockerfile sits outside every feedback loop we build for ourselves: no type checker, no test suite, no review culture around COPY ordering. If a tool needs setup before its first useful output, for infrastructure files it simply never runs.

What a scan looks like

npx @docker-doctor/cli@latest

No install, no config. Docker Doctor discovers the Dockerfiles and Compose files in your project, runs the rule set, and prints diagnostics with fix guidance, grouped under a score:

Here is the kind of file it is built to catch, one I have written more than once:

Dockerfile
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]

Six lines, at least four problems. node:latest makes the build non-reproducible; the base image can change underneath you between two builds of the same commit. COPY . . before npm install invalidates the dependency layer on every source change, so the build cache does nothing. npm install ignores the stricter lockfile semantics of npm ci. And nothing here drops root: the process runs as UID 0, which is the default nobody remembers to change.

That last one is the rule I care about most, and every rule can explain itself from the CLI:

npx @docker-doctor/cli@latest rules list
npx @docker-doctor/cli@latest rules explain docker-doctor/no-root-user

When a rule genuinely doesn't fit your project, severities can be overridden in a config file. The config is an escape hatch, not a prerequisite:

docker-doctor.config.ts
import type { DockerDoctorConfig } from "@docker-doctor/cli";

const config: DockerDoctorConfig = {
  rules: {
    "docker-doctor/no-root-user": "error",
  },
};

export default config;

After the first scan, the CLI also offers to wire the same check into a GitHub Actions workflow, so the score has somewhere to live beyond your terminal history.

How the score works

The score is where most of the design effort went, so it gets its own section. Every diagnostic adds a penalty based on severity:

SeverityPenalty
error10
warning4
info1

The obvious move is to subtract the total from 100. That has a failure mode on exactly the projects that need the tool most: a repository with years of accumulated Dockerfiles bottoms out at 0 almost immediately, and once the score is pinned there it stops saying anything. Fixing ten errors on a project stuck at 0 produces no visible change, which kills the motivation the score exists to create.

So the score is an asymptotic decay curve instead of a subtraction:

score = round(100 * e^(-penalty / K))   // K = 70

A clean project scores exactly 100, since e to the power of zero is one; no special-casing needed. As penalties pile up the score keeps falling but never pins to the floor, so a messy project that fixes things still sees the number move. K = 70 is tuned so a single warning (penalty 4) lands around 94, comfortably inside the Excellent bucket, because one warning should not feel like failure, while errors and repeated warnings keep eroding the score meaningfully. The labels sit on fixed thresholds: 90 and up is Excellent, 75 is Good, 50 is Needs Work, and below that is Critical.

Where it sits next to hadolint, dockle, and trivy

Docker Doctor is not trying to replace the existing container-security stack, and it is worth being precise about the boundaries:

ToolInputWhen it runsWhat it tells you
hadolintA single DockerfilePre-buildDockerfile lint violations, shell issues via ShellCheck
dockleA built imagePost-buildCIS-benchmark-style image checks
trivyImages, filesystems, reposPost-build / CIKnown CVEs in OS packages and dependencies
Docker DoctorThe whole project: Dockerfiles + ComposePre-buildHealth score, prioritized fix guidance, Compose-aware rules

The gap it fills is project-level and pre-build. hadolint sees one file at a time and has no opinion about your Compose topology; trivy reports on the image you already built. Docker Doctor reads the sources that produce the image and scores the project as a whole.

Sharp edges

Static analysis of Dockerfiles has real limits, and pretending otherwise would undermine the tool.

  • Build args hide information. FROM ${BASE_IMAGE} cannot be judged without knowing the value at build time, and a rule that guesses will be wrong in both directions. Some diagnostics are necessarily softer than the underlying risk.
  • The score is a proxy, not a guarantee. A 100 means the files follow the rules; it says nothing about CVEs in your base image. You still need a scanner like trivy in the pipeline, because a health score and a vulnerability report answer different questions.
  • Opinionated defaults are wrong somewhere. no-root-user is non-negotiable in most environments and pure noise in a rootless-container setup that already enforces it at the runtime layer. Downgrading a rule deliberately is healthy; silencing a whole category because one rule annoyed you is how tools die.
  • Compose is a moving target. The spec has absorbed years of drift (version keys that are now no-ops, fields that moved between releases), and rules have to target the current spec while most real-world files predate it.

The reason I keep building doctor-shaped tools is that the hard part of infrastructure hygiene was never knowledge. Everyone knows to pin base images and drop root; the docs have said so for a decade. What is missing is a feedback loop cheap enough to actually run. One command with a number at the end is the cheapest loop I know how to build.

Your Dockerfile is probably wrong. It now costs one command to find out why.

Theme
Select display theme
Email
hello@pungrumpy.com
Location
Bangkok, TH
Created by
Noppakorn Kaewsalabnil