What gets checked
Docker reads a Dockerfile line by line, and each logical line must start with a known instruction. The validator walks the file the same way, following \ line continuations and heredoc bodies (RUN <<EOF ... EOF), and checks two rules:
- Every instruction must exist.
FROM,RUN,CMD,LABEL,EXPOSE,ENV,ADD,COPY,ENTRYPOINT,VOLUME,USER,WORKDIR,ARG,ONBUILD,STOPSIGNAL,HEALTHCHECK,SHELLand the deprecatedMAINTAINERare accepted in any letter case. A typo such asCOPPYorFORMis an error, reported with its line, and the file is not formatted until it is fixed. - Only
ARGmay come before the firstFROM. ARUNorCOPYabove it is reported as a warning, because Docker rejects it but the rest of the file is still readable.
Comments, blank lines and parser directives such as # syntax=docker/dockerfile:1 are skipped.
The most common real-world trigger for an unknown-instruction error is not a typo at all but a lost backslash. When one line of a long RUN apt-get install ... \ chain loses its trailing \, the next line (curl \, say) becomes its own instruction, and the message names that command. If the reported word looks like a shell command, check the end of the line above it.
Two known limitations: the checker assumes the default backslash continuation character, so Windows images that switch to a backtick with the escape parser directive get false errors on their continuation lines; and a # comment placed between the lines of a continued RUN ends the continuation as far as the checker is concerned, although Docker accepts it. Move such comments above the instruction.
From validation to formatting
Once no errors remain, the file is passed to dprint’s Dockerfile plugin, compiled to WebAssembly, which normalises spacing and instruction layout while keeping your continuation lines. If you introduce an error later, the instruction name is shown in the message (Unknown instruction 'COPPY'), its line is highlighted, and the output pane holds on to the last formatted version in a dimmed style. Use Ctrl/Cmd+Enter to format and Ctrl/Cmd+Shift+C to copy.
Nothing is sent to a registry or a daemon; the check happens in your browser, so build arguments that reference private registries stay private.
What docker build checks that this page does not
The validator does not run BuildKit, so anything that depends on the build context or the network is out of scope: whether a base image tag exists, whether a COPY source path is present, whether npm ci will succeed. Instruction arguments are not parsed in detail either. An exec-form CMD with an unbalanced JSON array, for example CMD ["gunicorn", "app:app", passes here; Docker would then silently run it in shell form. Flags such as --mount or --chmod are not checked against your Docker version.
For best-practice linting (pin image versions, combine apt-get update with install, avoid latest), use hadolint or docker build --check. They complement this syntax check rather than replace it.
Examples
Misspelled COPY instruction
Invalid: COPPY on line 3 is not a Dockerfile instruction, so validation stops there.
FROM node:22-alpine
WORKDIR /app
COPPY package*.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
Line 3, column 1: Unknown instruction 'COPPY'FROM typed as FORM
Invalid: the very first instruction is misspelled, which docker build would also reject before doing anything.
FORM python:3.12-slim
WORKDIR /srv
RUN pip install --no-cache-dir flask gunicorn
Line 1, column 1: Unknown instruction 'FORM'
Line 2, column 1: WORKDIR before the first FROMARG before FROM in a multi-stage build
Passes: ARG is the one instruction allowed before FROM, and both stages are checked.
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci && npm run build
FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
Heredoc in a RUN instruction
Passes: the lines inside the heredoc are treated as script, not as Dockerfile instructions.
# syntax=docker/dockerfile:1
FROM debian:12-slim
RUN <<EOF
apt-get update
apt-get install -y --no-install-recommends curl
rm -rf /var/lib/apt/lists/*
EOF
# syntax=docker/dockerfile:1
FROM debian:12-slim
RUN <<EOF
apt-get update
apt-get install -y --no-install-recommends curl
rm -rf /var/lib/apt/lists/*
EOF
Common errors and how to fix them
| Error | Cause | Fix |
|---|---|---|
Unknown instruction 'COPPY' | The first word of a line is not a Dockerfile instruction, usually a typo or a shell command missing its RUN. | Correct the instruction name, or prefix shell commands with RUN. |
RUN before the first FROM | A build step appears above the base image; only ARG is allowed there. | Move the line below FROM, or turn it into an ARG if it defines a build argument. |
Unknown instruction 'npm' | A line continuation backslash is missing at the end of the previous RUN line, so the next line is read as a new instruction. | End the previous line with \ so the command continues. |
Frequently asked questions
Are lowercase instructions valid?
Yes. Docker treats instructions case-insensitively, and so does the validator. Uppercase is the convention; the formatter uppercases some instructions, such as FROM, RUN and COPY, but leaves others as written.
Does it pull images or check that tags exist?
No. Nothing is fetched. Base image names and tags are only checked by docker build or your registry.
Can it validate Containerfiles for Podman?
Yes. Containerfile uses the same syntax as a Dockerfile, including multi-stage builds and heredocs.
What is the difference between this and hadolint?
This page catches structural mistakes that stop a build. hadolint adds best-practice rules and runs ShellCheck on RUN commands, so use both.