16.1.1.3 Dockerignore Exclusion Issue
A focused guide to Dockerignore Exclusion Issue, connecting core concepts with practical Docker and container operations.
A dockerignore exclusion issue is a problem caused not by missing a .dockerignore file entirely, but by its pattern syntax behaving differently than expected, excluding too much, too little, or the wrong files altogether, which is a distinct and often more subtle category of problem than simply forgetting to add the file in the first place.
Pattern matching basics and common surprises
.dockerignore patterns are similar to .gitignore syntax but are evaluated by Docker's own implementation, which has some specific behaviors worth understanding precisely rather than assuming based on familiarity with Git's ignore handling:
*.log
temp/
!important.log
A pattern like *.log matches log files anywhere in the context by default, not just at the root, which surprises people expecting it to match only top-level files unless a leading slash or more specific path is used.
Negation patterns and their limitations
The ! prefix re-includes a file that would otherwise be excluded by an earlier pattern, but it has an important limitation: it cannot re-include a file if one of its parent directories was already excluded, since Docker does not traverse into a directory that has already been excluded to check for negation patterns inside it:
logs/
!logs/important.log
This negation does not work as might be expected, since the entire logs/ directory is excluded first, and Docker never looks inside an excluded directory to find the negation pattern that would have re-included a specific file within it. The fix is excluding individual files rather than the entire parent directory, or restructuring to keep needed files outside the excluded directory entirely:
logs/*
!logs/important.log
Excluding the directory's contents with a trailing /* rather than excluding the directory itself allows Docker to still traverse into it and apply the subsequent negation pattern correctly.
Trailing slash behavior
A pattern ending in a slash matches only directories, not files with the same name, which is a meaningful distinction when a project happens to have both a file and a directory sharing a name, or when a pattern is intended to match more broadly than a trailing slash actually allows:
build/
build
The first pattern excludes only a directory literally named build; the second, without the trailing slash, matches either a file or directory with that name, which is a subtle but occasionally consequential difference.
Double-star wildcard behavior
The ** pattern matches across multiple directory levels, which is necessary for excluding a pattern regardless of how deeply nested it is, as opposed to a single * which does not cross directory boundaries in the same way:
**/*.test.js
*.test.js
The first pattern excludes test files at any depth within the context; the second matches only files at the root level, which is a common source of an exclusion pattern appearing to work in a simple project structure but failing once files move into nested subdirectories.
Order and precedence of patterns
Patterns are evaluated in the order they appear in the file, with later patterns able to override earlier ones, including re-including something an earlier, broader pattern excluded; getting the order wrong, particularly placing a negation before the pattern it is meant to override, produces no effect at all:
!important.log
*.log
In this order, the negation has no effect, since the broader exclusion pattern comes after it and re-excludes the file the negation had just included; correct ordering requires the broad exclusion first, followed by the more specific negation:
*.log
!important.log
The .dockerignore location requirement
The .dockerignore file must be located at the root of the build context being used, and a file placed in a different directory, such as alongside the Dockerfile when the Dockerfile lives in a different location than the context root, is silently ignored entirely, with no error or warning indicating it was never actually applied:
docker build -f docker/Dockerfile -t my-api .
docker/.dockerignore # WRONG location, has no effect
.dockerignore # correct location, at the context root
Because the build context in this example is the repository root (.), the .dockerignore file needs to live there as well, not inside the docker/ subdirectory where the Dockerfile happens to be, which is a frequent source of a .dockerignore file that appears correctly written but has simply never been applied at all.
Comments and blank lines
Lines beginning with # are treated as comments and ignored, and blank lines are also ignored, both of which are useful for documenting the intent behind specific patterns in a larger, more complex .dockerignore file:
# Exclude dependency directories built on the host
node_modules
# Exclude local environment files; secrets should never reach the build context
.env
.env.*
Verifying actual exclusion behavior
Because .dockerignore issues are often subtle pattern-matching mistakes rather than outright missing functionality, directly verifying what is and is not included in a given build context is more reliable than reasoning about pattern behavior abstractly:
docker build --progress=plain -t my-api . 2>&1 | grep "Sending build context"
docker run --rm -v "$(pwd)":/context alpine sh -c "cd /context && find . -name 'node_modules' -prune -o -print" | wc -l
For especially stubborn cases, temporarily adding a build step that lists the actual context contents received by the daemon (for instance, a RUN find . | sort early in the Dockerfile) directly confirms what files actually arrived, removing any ambiguity about whether the .dockerignore patterns behaved as intended.
COPY . /tmp/context-check
RUN find /tmp/context-check -maxdepth 2
Common mistakes
- Excluding an entire directory and then attempting to negate a specific file within it, not realizing Docker cannot traverse into an already-excluded directory to apply the negation.
- Using a single
*wildcard expecting it to match across multiple directory levels the way**does. - Placing a negation pattern before the broader exclusion pattern it was meant to override, resulting in no effect at all due to pattern evaluation order.
- Placing
.dockerignorein the wrong location relative to the actual build context root, silently causing it to have no effect whatsoever. - Assuming
.dockerignorebehaves identically to.gitignorein every respect, rather than verifying the specific behaviors that genuinely differ between the two.
Dockerignore exclusion issues are almost always traceable to a specific, identifiable pattern-matching detail, negation and directory traversal limitations, double-star versus single-star scope, pattern ordering, or file location, and directly verifying actual context contents rather than reasoning abstractly about expected pattern behavior is the fastest way to confirm or rule out each of these specific causes.