Dockerfile best practices linter with line numbers
This Dockerfile best practices linter gives a focused review of three common problems before an image reaches a build system.
Run — free
It reports a missing WORKDIR, base images that use the mutable latest tag, and consecutive RUN instructions that may create unnecessary layers. Every finding includes a one-based source line, a stable type, and a direct explanation, making the result useful both to a developer reading a file and to an automated check in continuous integration.
Read a small, actionable Dockerfile report
Paste the complete Dockerfile content into the text field and run the check. The response says whether the file passed, gives the number of findings, and lists each issue with a one-based line number, a stable machine-readable type, and a concise message. The linter deliberately concentrates on three frequent maintenance problems instead of pretending to replace a full container security suite. A missing WORKDIR is reported at line 1 because it applies to the file as a whole. A FROM instruction is reported on its own source line when its image explicitly uses the latest tag or omits a tag and therefore resolves to latest. When two RUN instructions are consecutive, the later instruction is reported and points back to the earlier line. Blank lines and comments do not hide that relationship. A clean file returns an empty findings array, which lets a script use valid as a simple pass condition without interpreting prose or filling in missing values. Line continuations are treated as part of one logical instruction, while their reported position remains the first physical source line so it matches an editor and a code review diff.
Understand why these three practices matter
WORKDIR makes the filesystem context explicit for later RUN, COPY, CMD, and ENTRYPOINT behavior. Without it, the build silently inherits a directory chosen by the base image, and a future base-image update can change where files are installed or commands execute. Pinning a FROM image to a versioned tag or immutable digest makes builds easier to reproduce. An untagged image reference and an explicit latest tag can both move to different content even when the Dockerfile itself has not changed. Finally, every RUN normally creates a filesystem layer. Consecutive package installation, cleanup, or setup commands often belong in one shell operation so temporary files can be removed in the same layer and the image history stays easier to understand. The finding is phrased as a recommendation because separate RUN instructions are sometimes intentional, especially when cache boundaries improve an established workflow. This tool does not rewrite commands or claim that every merge is safe. It gives reviewers an exact location and leaves the final decision to someone who understands the build, its caching strategy, and the failure behavior of the shell commands involved.
Place the check before expensive image builds
Run the linter in an editor action, a pre-commit workflow, or an early continuous-integration job before downloading base images and compiling an application. Send the original source text rather than a parsed representation so physical line numbers and continued instructions remain accurate. The algorithm is deterministic: identical input produces identical output, and it does not use the network, a clock, random values, a Docker daemon, or environment-specific state. That makes it suitable for gating generated Dockerfiles as well as files maintained by hand. Treat the report as a basic maintainability signal, not proof that an image is secure or buildable. The linter does not execute shell commands, resolve variables in image names, inspect package versions, validate COPY sources, enforce a non-root USER, or scan image layers for vulnerabilities. A FROM value containing build-time variable expansion may therefore need human review. Combine this fast text check with an actual container build, image vulnerability scanning, policy enforcement, and tests for the resulting process. If the input contains only whitespace or comments, the request fails as invalid input because there are no Dockerfile instructions to assess, which prevents an empty file from being mistaken for a successful review.
What you can do with it
Review a Dockerfile before commit
Catch mutable base-image references and unclear working-directory behavior while the relevant lines are still being edited.
Gate generated container definitions
Check template output for basic maintainability rules before a pipeline spends time building and publishing an image.
Triage a container repository
Produce consistent line-numbered findings that help a team prioritize simple Dockerfile cleanup across services.
FAQ
What does one lint request cost?
Each API request costs $0.002. The browser version can also run directly on this page.
Does an image without a tag count as latest?
Yes. Docker treats an omitted tag as latest, so the linter recommends an explicit versioned tag or digest.
Does the tool automatically combine RUN instructions?
No. It reports consecutive RUN instructions but does not rewrite them, because separate cache boundaries can be intentional.
Why is a missing WORKDIR reported on line 1?
The omission applies to the whole file and has no source line of its own, so line 1 is used as the file-level location.
Does this validate Dockerfile syntax or image security?
No. It performs three basic best-practice checks and should be combined with a real build, policy checks, and vulnerability scanning.
For developers — API access
Everything on this page is available programmatically. This section is for teams who want to wire it into their own systems; everyone else can just use the tool above.
API endpoint
Prefer to automate it? One authenticated POST creates the task; the result comes back by webhook or a signed link. The same capability also runs here on the web, by email and from Telegram — and soon from our app too.
Call it from your stack
curl -X POST https://api.kit.forhosting.com/dev2/dockerfile-lint-basic \
-H "Authorization: Bearer $KIT_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci && npm cache clean --force\nCOPY . .\nCMD [\"node\", \"server.js\"]"}'const res = await fetch("https://api.kit.forhosting.com/dev2/dockerfile-lint-basic", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.KIT_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
"text": "FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci && npm cache clean --force\nCOPY . .\nCMD [\"node\", \"server.js\"]"
})
});
const { task_id } = await res.json();import os, requests
res = requests.post(
"https://api.kit.forhosting.com/dev2/dockerfile-lint-basic",
headers={"Authorization": f"Bearer {os.environ['KIT_KEY']}"},
json={
"text": "FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci && npm cache clean --force\nCOPY . .\nCMD [\"node\", \"server.js\"]"
},
)
task_id = res.json()["task_id"]<?php
$res = file_get_contents("https://api.kit.forhosting.com/dev2/dockerfile-lint-basic", false, stream_context_create([
"http" => [
"method" => "POST",
"header" => "Authorization: Bearer " . getenv("KIT_KEY") . "\r\nContent-Type: application/json",
"content" => '{"text":"FROM node:20-alpine\\nWORKDIR /app\\nCOPY package*.json ./\\nRUN npm ci && npm cache clean --force\\nCOPY . .\\nCMD [\\"node\\", \\"server.js\\"]"}',
],
]));
$task = json_decode($res, true);body := bytes.NewBufferString(`{"text":"FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci && npm cache clean --force\nCOPY . .\nCMD [\"node\", \"server.js\"]"}`)
req, _ := http.NewRequest("POST", "https://api.kit.forhosting.com/dev2/dockerfile-lint-basic", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("KIT_KEY"))
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)Example request
{
"text": "FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci && npm cache clean --force\nCOPY . .\nCMD [\"node\", \"server.js\"]"
}Example response
{
"task_id": "tsk_a1b2c3d4e5f6a1b2c3d4e5f6",
"type": "dev2.dockerfile_lint_basic",
"status": "queued",
"_links": {
"result": "/tasks/tsk_…/result"
}
}The API is asynchronous: the call returns a task_id immediately and the result arrives by webhook. Polling is capped at 1 req/s per task.
Pricing
Published price — no tokens, no invented credits. A failed task is never charged.
Errors
| HTTP | Code | Meaning |
|---|---|---|
401 | unauthorized | Missing or invalid API key. |
402 | insufficient_balance | Your balance doesn't cover the task price. |
404 | unknown_type | That task type doesn't exist. |
429 | rate_limited | Too many requests. Use the webhook instead of polling. |