BuildKit’s Heredoc support isn’t just a convenience; it fundamentally changes how you can inject dynamic content into your Docker image builds, bypassing traditional multi-stage build limitations for certain tasks.

Let’s see it in action. Imagine you need to create a configuration file within your image that depends on an environment variable set during the build.

# syntax=docker/dockerfile:1
FROM alpine:latest
ARG APP_CONFIG_VALUE="default_value"

RUN --mount=type=bind,target=/app/config.json <<EOF
echo '{
  "setting": "${APP_CONFIG_VALUE}",
  "timestamp": $(date +%s)
}' > /app/config.json
EOF

CMD ["cat", "/app/config.json"]

When you build this with BuildKit enabled:

DOCKER_BUILDKIT=1 docker build -t heredoc-example .

And then run it:

docker run --rm heredoc-example

You’ll see output like:

{
  "setting": "default_value",
  "timestamp": 1678886400
}

If you override the ARG:

DOCKER_BUILDKIT=1 docker build --build-arg APP_CONFIG_VALUE="production" -t heredoc-example .
docker run --rm heredoc-example

The output changes:

{
  "setting": "production",
  "timestamp": 1678886401
}

This demonstrates how RUN --mount=type=bind can be used with Heredoc to create files dynamically, incorporating build arguments and even shell commands within the Heredoc itself. The EOF marker signifies the end of the Heredoc content, which is then passed as standard input to the RUN command. The --mount=type=bind,target=/app/config.json part is crucial: it tells BuildKit to mount a temporary directory to /app/config.json inside the build container. The echo command then writes to this mounted location, effectively creating the config.json file within the image’s filesystem at that path.

The core problem Heredoc syntax in RUN commands with BuildKit solves is the inability to easily generate file content dynamically within a single RUN instruction, especially when that content needs to incorporate build-time variables or shell expansions. Before this, you might have resorted to complex sed or awk commands in a multi-stage build, or even created a separate script file and copied it in. Heredoc provides a clean, inline way to do this. The <<EOF syntax means "read input until you see a line containing only EOF". Everything between that and the EOF line is treated as the standard input for the RUN command.

The RUN --mount=type=bind is the mechanism that makes this work for file creation. Without the mount, the echo command would just print to stdout of the build container. By binding a target path, the command’s output is redirected to a file at that path within the build context. This is powerful because it allows you to write complex file contents, including shell logic and variable substitutions, directly in your Dockerfile without needing separate COPY or ADD instructions for generated files.

You control the content of the generated file through the text within the Heredoc, and you can inject dynamic values using standard shell variable expansion (like ${APP_CONFIG_VALUE}) or command substitution (like $(date +%s)). BuildKit ensures these expansions happen correctly within the isolated build environment. The --mount option’s target parameter specifies where the output file will reside in the final image.

What’s often overlooked is that the RUN command within the Heredoc can be any executable. You’re not limited to echo. You could have a Python script, a shell script, or even a compiled program executed within the Heredoc to generate highly complex configuration files or data structures. The --mount=type=bind simply captures the standard output of that execution and writes it to the specified target file.

The next logical step after mastering dynamic file generation with Heredocs is to explore how BuildKit’s caching mechanisms interact with these --mount operations, particularly when dealing with frequently changing dynamic content.

Want structured learning?

Take the full Buildkit course →