Skip to content

Scoring

How the 0-100 health score and its label are calculated.

On this page

Every scan that analyzes at least one file produces a 0-100 health score alongside a label:

LabelThreshold
Excellent 🏆score >= 90
Good ✅score >= 75
Needs Work ⚠️score >= 50
Critical 🚨score < 50

Penalty per diagnostic

Each diagnostic adds a penalty based on its severity:

SeverityPenalty
error10
warning4
info1

Averaged per file

docker-doctor adds up the penalties and divides the total by the number of Dockerfiles and Compose files it analyzed. The score therefore describes a typical file in the project, and a clean file counts as much as a bad one.

penalty = sum of penalties / analyzed files

A project with one Docker file is scored on that file alone. A monorepo with 20 files and two warnings in each scores the same 89 as a single file with two warnings.

A file counts as analyzed when docker-doctor parsed it and the rules have something to check in it. A Dockerfile needs at least one instruction. A Compose file needs a top-level mapping with at least one service, a top-level models entry or a version key. Files that could not be parsed, empty files and files with nothing to check are left out of the count, so they do not pull the average up. The JSON report gives the count as analyzedFiles.

When no file was analyzed, there is no score. That covers a directory with no Dockerfile or Compose file, files that all failed to parse, and files with nothing to check. --json reports score and label as null, --score prints nothing, and the GitHub Action leaves its score and label outputs empty. The exit code is 2 when files failed to parse and 0 otherwise.

Releases before 0.7.0 used the sum without dividing it. A repository with many Docker files scored near 0 however small each file's problems were.

The average can hide one bad file among many good ones. The exit code does not depend on the score, so an error-severity finding in any file still fails the scan.

The curve

The score is an asymptotic decay curve over that penalty, not a simple subtraction:

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

A project with no diagnostics always scores exactly 100. As the penalty grows, the score falls toward 0 without a hard floor. Rounding gives 0 once the average penalty reaches about 371 per file, which is roughly 37 errors in every file. Below that, the score keeps responding to fixes, though one small finding in a large project can round away.

K = 70 was chosen so a single warning (penalty 4) still lands around 94 — comfortably inside the Excellent bucket — while errors and repeated warnings continue to meaningfully erode the score. The previous K = 40 pushed a single warning down to ~90, right on the Excellent/Good boundary — too harsh a penalty for one warning.

The old max(0, 100 - penalty) formula saturated at 0 once penalty reached 100 (about 10 errors), making a messy project and a catastrophic one indistinguishable, and the score could never register a fix. The exponential curve stays monotonic and responsive across the whole range instead.

Using the score in CI

npx @docker-doctor/cli@latest . --score

Prints only the numeric score, and nothing at all when there is no score. The exit code follows the same rule as every other mode: non-zero when any error-severity diagnostic is present, so a CI gate behaves identically whichever output you pick.