Description
Suggests replacingRUN echo/cat/printf > file patterns with COPY <<EOF syntax for better performance and readability.
This rule detects file creation patterns in RUN instructions and extracts them into COPY heredocs, even when mixed with other commands.
It relies on Dockerfile here-documents support for COPY.
Why COPY heredoc?
Three reasons, in order of importance:-
Hermeticity and intent. A
RUNthat writes a file is an opaque shell invocation: the frontend cannot tell, without parsing your shell, whether the instruction installs a package, mutates system state, or just drops a config file.COPY <<EOFdeclares the same operation as a pure, fully specified input-to-output mapping. See Bazel’s hermeticity guide for the underlying principle — a build step whose output is a function of its declared inputs is easier to cache, reason about, and reproduce. -
Observable content for other rules. tally tracks files written via
COPY <<EOF(andADD <<EOF) as observable files: their content is visible at lint time. This lets other rules reason about what the image actually contains. For example:tally/secrets-in-codescans heredoc bodies for API keys and tokens.tally/user-created-but-never-usedinspects/etc/passwd-style content to learn about created users.tally/php/no-xdebug-in-final-imagereadsphp.ini-style config to detect Xdebug activation.
RUN echo ... > /pathis not observable — tally can only see the shell source, not the final file — so downstream rules give up, and the rule set catches fewer real issues in your image. -
Predictable cache keys.
COPY <<EOFparticipates in BuildKit’s content cache: the layer hash is derived from the literal heredoc body, so the layer hits cache whenever the content is unchanged. ARUNlayer hashes over the command string, so any innocuous edit (trailing whitespace, reordered&&clauses, comments) busts the cache even when the file it produces is identical. Splitting “create this file” into its ownCOPYkeeps it out of the rebuild path of unrelated shell changes.
COPY --chmod sets permissions in a single layer (no
follow-up chmod), and heredoc bodies are easier to read than escaped
echo statements with manual \n separators.
Detected Patterns
- Simple file creation:
echo "content" > /path/to/file - printf with escape sequences:
printf 'line1\nline2\n' > /path/to/file - File creation with chmod:
echo "x" > /file && chmod 0755 /file - BuildKit heredoc piped to cat:
RUN <<EOF cat > /path/to/file - BuildKit heredoc piped to tee:
RUN <<EOF tee /path/to/file - Brace-grouped producers piped to tee:
{ echo a; printf 'b\n'; } | tee /path(mixes ofecho/printf/cat <<EOFare supported) - Consecutive RUN instructions writing to the same file
- Mixed commands with file creation in the middle (extracts just the file creation)
- Multiple distinct targets in one RUN: a single
&&chain that writes to several files is split into oneCOPY <<EOFper target. Anymkdir -p /parentwhose target is a prefix of a COPY destination is dropped, sinceCOPYauto-creates parent directories.
Examples
Before (violation)
After (fixed with —fix —fix-unsafe)
COPY <<EOF blocks are surrounded by blank lines to keep the embedded
file content readable; the fix doesn’t inject duplicate blanks when the source
already has one adjacent to the replacement.
Multi-target with brace-grouped pipes
A singleRUN that builds several config files via { echo ...; echo ...; } | tee /path
chains — a common pattern in official images such as php:fpm — expands into one
COPY <<EOF per destination. mkdir -p is absorbed when a later COPY writes under
the same directory tree, and a leading set -ex is dropped entirely (shell options
don’t cross RUN boundaries, so preserving it in a standalone RUN would be pure noise).
Before (violation)
After (fixed with --fix --fix-unsafe)
Limitations
- Skips append operations (
>>) since COPY would change semantics - Skips relative paths (only absolute paths like
/etc/file) - Skips commands with shell variables not defined as ARG/ENV
Mount Handling
SinceCOPY doesn’t support --mount flags, the rule handles RUN mounts carefully:
When extracting file creation from mixed commands, mounts are preserved on the remaining RUN instructions.
Chmod Support
Preserves the original mode notation onCOPY --chmod. COPY --chmod
accepts both octal and symbolic modes (Dockerfile frontend 1.14+), so the
fixer emits whichever form the source wrote:
- Octal:
chmod 755→--chmod=755,chmod 0755→--chmod=0755 - Symbolic:
chmod +x→--chmod=+x,chmod u+x→--chmod=u+x
Options
Configuration
Rule Coordination
This rule takes priority overprefer-run-heredoc for pure file creation patterns. When both rules detect a pattern, prefer-copy-heredoc handles
it.
References
- Dockerfile here-documents
- Introduction to heredocs in Dockerfiles
- Bazel: hermeticity — the build-system principle behind motivation #1