BuildKit’s docker build --dry-run is your secret weapon for catching Dockerfile mistakes before they even hit the build process, preventing wasted compute and debugging time.
Let’s see it in action. Imagine this simple, slightly flawed Dockerfile:
FROM alpine:latest
RUN echo "Hello, world!" > /hello.txt
COPY . /app
CMD ["/app/run.sh"]
Now, let’s lint it with BuildKit’s dry-run feature. You’ll need to have BuildKit enabled in your Docker daemon (it’s the default in recent versions, but if you’re unsure, check your daemon.json for "features": {"buildkit": true}).
The command is straightforward:
docker build --dry-run -f Dockerfile .
What happens? BuildKit intercepts the build. It doesn’t actually run commands, pull images, or create layers. Instead, it parses your Dockerfile, analyzes the instructions, and simulates the build process to identify potential issues. It’s like a static analysis tool for your Dockerfiles, but with a deeper understanding of the build lifecycle.
The output you’ll see is a JSON representation of the build plan. It’s not an error message in the traditional sense, but a detailed breakdown of the steps BuildKit would take. This plan is where the magic happens. You’re essentially debugging the build definition itself.
The core problem this solves is the implicit trust we place in Dockerfile instructions. We assume RUN commands will succeed, COPY will find files, and CMD will execute a valid program. BuildKit’s dry run challenges that assumption by performing a pre-flight check.
Here’s what BuildKit is checking under the hood:
- Syntax Errors: Obvious, but crucial. It catches typos, missing arguments, or incorrect instruction formatting.
- Unreachable Stages: If you have multi-stage builds, it can detect if a stage is defined but never used as a source for a subsequent
COPY --from. - Invalid
COPYSources: It checks if the files or directories specified inCOPYorADDinstructions actually exist in the build context. This is a huge time-saver, preventing builds from failing halfway through because a required file was missing. - Potential
CMD/ENTRYPOINTIssues: While it can’t run your script to know if it has bugs, it can flag if theCMDorENTRYPOINTpoints to a file that isn’t copied into the image or isn’t executable. - Dependency Issues (Limited): For
RUNcommands that involve package managers (likeapt-get,apk), BuildKit can sometimes infer if the package installation command itself is syntactically correct, though it won’t check if the package exists in the repository. - Build Context Size: While not a direct error, the dry-run plan can give you an idea of how many files are being sent to the daemon, indirectly flagging overly large build contexts.
Let’s refine our Dockerfile based on a hypothetical dry-run output that might indicate a problem (BuildKit’s dry-run output is verbose JSON, but we’ll simulate the implication of an error). Suppose the dry run reveals that the CMD instruction references /app/run.sh, but no RUN or COPY instruction ever placed run.sh in the /app directory.
The corrected Dockerfile would look like this:
FROM alpine:latest
RUN echo "Hello, world!" > /hello.txt
COPY run.sh /app/run.sh # Assuming run.sh is in your build context
RUN chmod +x /app/run.sh # Make it executable
CMD ["/app/run.sh"]
By adding the COPY and chmod +x instructions, we ensure that the run.sh script is present and executable at the path specified in the CMD. The dry run would have highlighted the missing artifact or the missing execute permission implicitly by showing a step that would fail.
The next error you’ll likely hit after perfecting your Dockerfile linting is related to the actual execution of your application, often manifesting as a non-zero exit code from your CMD or ENTRYPOINT, indicating a runtime problem within your application’s logic.