✦ For everyone, free.

Practical knowledge for real and everyday life

Home

17.1.1.4 Unneeded File Avoidance

A focused guide to Unneeded File Avoidance, connecting core concepts with practical Docker and container operations.

Unneeded file avoidance is the practice of deliberately controlling exactly what ends up inside an image's layers, beyond just excluding development dependencies or choosing a minimal base, addressing source-level files, documentation, tests, version control metadata, build artifacts from other targets, that have no legitimate reason to exist inside a running production container at all.

Maintaining a thorough .dockerignore proactively

A .dockerignore file should be maintained as an active, reviewed part of the project, not written once and forgotten, since new directories and file types are introduced over a project's lifetime that should be excluded from the build context, and therefore from any risk of being copied into the image, from the moment they are introduced:

.git
.github
node_modules
*.md
test/
docs/
.env
.env.*
coverage/
*.log

Treating this file with the same ongoing attention as any other piece of project configuration, rather than as a one-time setup task, keeps it accurate as the project's own structure evolves over time.

Being deliberate about COPY granularity

A broad COPY . . instruction copies everything in the build context not already excluded by .dockerignore, which is convenient but less precise than explicitly copying only the specific files and directories an image actually needs:

COPY . .
COPY src/ ./src/
COPY package.json package-lock.json .
COPY public/ ./public/

The more granular approach is more verbose but makes explicit exactly what the image actually contains, which is both a documentation benefit, anyone reading the Dockerfile understands precisely what is included, and a more robust safeguard against an unexpected new file or directory being swept in automatically by a broad copy instruction simply because no one thought to add it to .dockerignore.

Excluding documentation and non-runtime files

README files, contributor guidelines, architecture documentation, and similar files serve a genuinely useful purpose in a repository but have no functional role inside a running container, and including them only adds unnecessary size with zero runtime benefit:

COPY README.md .
COPY CONTRIBUTING.md .
COPY docs/ ./docs/

Unless an application specifically serves its own documentation as part of its runtime behavior (which is uncommon and would be a deliberate, specific exception), these files should be excluded from the image entirely through .dockerignore rather than copied in by a broad instruction and left unaddressed.

Excluding test files and fixtures

Test source code, test fixtures, and mock data have no role in a production runtime and should be excluded the same way development dependencies are, since their continued presence in the final image provides no benefit while still contributing to size and, in the case of fixtures containing realistic-looking but fake data, occasional confusion if mistaken for real configuration:

test/
__tests__/
*.test.js
*.spec.js
fixtures/

This exclusion is particularly important to verify directly, since a test fixture file with a name resembling real configuration, such as a fake .env.test file, could otherwise be mistaken for genuine configuration if it ends up unexpectedly present in a production image.

Avoiding leftover build artifacts from other targets

In a multi-target or multi-stage setup, ensuring only the artifacts genuinely relevant to the specific final stage being built are actually copied into it, rather than artifacts from an unrelated target or an earlier, now-irrelevant build attempt, requires being explicit about exactly which paths each COPY --from= instruction references:

COPY --from=build /app/dist ./dist
COPY --from=build /app ./

The second example copies the entire build stage's filesystem, including its own node_modules, source files, and any other build-time artifacts, rather than the specific, narrow output directory that actually needs to ship; being explicit about the narrowest path that genuinely satisfies the final image's needs avoids accidentally carrying forward considerably more than intended.

Avoiding stray local configuration and credentials

Local development configuration files, IDE settings, and especially any file that might contain credentials or secrets, should never be present in the build context in a way that risks being copied into an image, since an image, once built and pushed, can be considerably harder to fully purge of an accidentally included sensitive file than a local working directory would be:

.env
.env.local
.vscode/
.idea/
*.pem
*.key

Excluding these explicitly, rather than relying on never accidentally referencing them in a COPY instruction, provides a more robust safeguard, since .dockerignore exclusion prevents the file from ever being available to copy in the first place, regardless of how broad or narrow any specific COPY instruction happens to be.

Verifying the final image's actual contents directly

Periodically inspecting an actually built image's filesystem directly confirms whether unneeded files have crept in despite the intended exclusions, which is a more reliable check than only reviewing the Dockerfile and .dockerignore file's apparent intent:

docker run --rm my-api:1.4.2 find / -maxdepth 3 -type f 2>/dev/null | grep -E '\.(md|test\.js|env)$'

A non-empty result from a check like this directly confirms unneeded files have made their way into the image despite the apparent exclusions, which is worth investigating and correcting at its source, whether a .dockerignore gap, an overly broad COPY instruction, or an unintended artifact carried forward from an earlier build stage.

Common mistakes

  • Writing .dockerignore once at project setup and never revisiting it as the project's structure evolves over time.
  • Relying entirely on a single, broad COPY . . instruction rather than considering more granular, explicit copying for better documentation and robustness against future, unconsidered additions.
  • Leaving documentation and test files unaddressed in .dockerignore, allowing them to be copied into the final image with no corresponding runtime benefit.
  • Copying an entire build stage's filesystem with COPY --from= rather than the specific, narrow output path that actually needs to ship.
  • Never directly inspecting a built image's actual contents to verify exclusions are working as intended, relying solely on the Dockerfile and .dockerignore file's apparent correctness.

Unneeded file avoidance is achieved through an actively maintained .dockerignore file, deliberate, granular COPY instructions rather than broad, all-encompassing ones, careful scoping of what is copied forward from earlier build stages, and periodic, direct verification of the actual built image's contents, all of which together keep an image containing exactly what it needs to run and nothing more.