Docker COPY vs ADD: 5 Critical Rules Pros Follow Daily
ADD auto-extracts tarballs and fetches URLs — COPY doesn't.
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
- ✓You can write and build a basic Dockerfile
- ✓You understand image layers and build cache basics
- ✓You've used multi-stage builds at least once
- COPY copies local files verbatim; ADD adds tar auto-extraction, remote URL fetching, and checksum validation on top
- Use COPY for everything local by default; reach for ADD only for remote artifacts with --checksum or local tarballs you truly want extracted
- Performance insight: one ADD of a changing remote URL invalidates every layer below it — teams have seen 1.2 GB rebuilds from a single ADD line
- Production insight: ADD's silent tar extraction once overwrote /app/config with archive contents in a deploy — explicit RUN curl + tar is auditable, magic is not
- Rule: COPY first, ADD on exception, bind mounts for build-only files, and always pin remote artifacts with checksums
Think of building a Docker image like packing a shipping container. COPY is the careful mover who places each labeled box exactly where you point — nothing more. ADD is the mover who also opens any suitcase he finds and unpacks it, and who'll drive across town to fetch a package from a URL you hand him. Sometimes you want that extra service. Most days you just want your boxes placed untouched, because surprise unpacking breaks the careful arrangement you planned.
Every Dockerfile tutorial whispers the same advice: prefer COPY over ADD. Then every team inherits a Dockerfile with ADD everywhere and nobody remembers why the rule exists.
The confusion is fair — both instructions put files into images, and for plain local files they behave identically. The difference hides in the extras: ADD auto-extracts tarballs and downloads remote URLs, COPY never does. Those extras look convenient until they invalidate your build cache or silently unpack 400 files over your config.
You'll get five rules, not one. They cover when ADD earns its keep (checksummed remote artifacts), when COPY wins (everything else), and the modern third option — bind mounts — that beats both for build-only files.
What Each Instruction Actually Does
COPY src dest takes files from the build context (or --from=stage) and duplicates them into the image. Ownership, permissions, and bytes are preserved. No network, no extraction, no surprises.
ADD src dest does the same for plain files, then adds two behaviors: local tar files (including .tar.gz) are extracted into the destination directory, and remote http(s) or Git URLs are fetched over the network into the image. Recent BuildKit adds --checksum and --keep-git-dir refinements to the remote path.
That 'same plus extras' framing explains every rule below. If you need zero extras, COPY's predictability is free. If you need an extra, you pay with cache and auditability questions — so use ADD deliberately, not habitually.
Rule 1: Default to COPY for Local Files
For requirements.txt, source trees, and configs, COPY is strictly better: identical results to ADD with zero magic. COPY app/ /app/ places bytes; reviewers see bytes. Cache invalidation follows file checksums, nothing else.
ADD on the same inputs behaves identically today but invites tomorrow's surprise — someone swaps app.tar.gz into that line and extraction starts silently. COPY can't change meaning under you.
Make it a lint rule: hadolint DL3014/DL3020 flag ADD usage, and every ADD needs a comment justifying the extra. Defaults should be boring; exceptions should explain themselves.
Rule 2: Use ADD Only for Checksummed Remote Artifacts
ADD earns its place downloading a pinned remote artifact in one layer with integrity verification: ADD --checksum=sha256:270d... https://example.com/tool.tar.gz /tmp/tool.tar.gz. The checksum pins the bytes; a changed upstream fails loudly instead of shipping silently.
Without --checksum, remote ADD is a reproducibility hole — rebuilds fetch whatever the URL serves today, and any byte change busts the cache for every layer below. That's how a one-line ADD triggers a 1.2 GB full rebuild.
Even then, consider RUN curl -fsSL --output with explicit retry flags and a verified digest. curl shows in the layer history exactly what ran; ADD hides the fetch behind instruction semantics. Both are defensible — unpinned remote ADD is not.
Rule 3: Never Rely on Silent Tar Extraction
ADD's auto-extraction merges archive contents into the destination, including nested directories that collide with your files. No manifest, no --strip-components equivalent, no dry run. The Friday deploy proved the cost.
The explicit pattern is two lines and fully reviewable: COPY bundle.tar.gz /tmp/ followed by RUN tar -xzf /tmp/bundle.tar.gz -C /app/vendor --strip-components=1 && rm /tmp/bundle.tar.gz. Reviewers see the strip level, the target, and the cleanup.
If you genuinely want ADD extraction (rare), comment the expected top-level paths in the Dockerfile so the next reader knows what should appear. Unexplained ADD of an archive is a review red flag.
Rule 4: Bind Mounts Beat Both for Build-Only Files
requirements.txt, .npmrc, and private repos needed only during RUN shouldn't persist in any layer. COPY bakes them in (leaking secrets into history); ADD can't help. Bind mounts solve it: RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt pip install -r /tmp/requirements.txt leaves no trace in the final image.
Secrets get their own mount: RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci reads the credential at build time without storing it. Combined with multi-stage builds (COPY --from=builder), the final image holds only compiled output.
Adopt the mantra: COPY for files that ship, mounts for files that build. Image sizes shrink, secret scans go quiet, and .dockerignore stops being load-bearing for security.
Rule 5: Permissions, .dockerignore, and Cache Order
Three finishing details separate senior Dockerfiles. First, set ownership inline: COPY --chown=app:app --chmod=0755 entrypoint.sh /app/ avoids a follow-up RUN chown that duplicates the layer's bytes.
Second, curate .dockerignore like .gitignore's stricter sibling: exclude .git, node_modules, *.log, and test fixtures so COPY . . doesn't ship 2 GB of junk or bust cache on irrelevant edits.
Third, order layers by change frequency: stable base deps first (requirements.txt + pip install), volatile app code last. A requirements-only change rebuilds one layer; app-code-first ordering rebuilds everything below it. These three habits compound into 10x faster rebuilds.
Decision Flowchart You'll Actually Remember
Ask one question: does this file ship in the final image? No → bind mount (or secret mount). Yes → next question: is it remote? Remote with a real need → ADD with --checksum. Remote otherwise → RUN curl explicitly.
Local file shipping in the image → COPY, always. Local tarball you want extracted → COPY plus explicit RUN tar. That's the whole flowchart, and it fits on a sticky note.
Tape it to the review checklist: every ADD must answer 'which extra do I need and where's the checksum?' If the author can't answer, it's a COPY.
The Tarball That Ate /app/config on Deploy Friday
- Implicit behavior is a deploy risk: prefer explicit COPY + RUN tar so every file placement is visible and reviewable.
- Assert image contents and size in CI — a 3x size jump or a config checksum mismatch should fail the build, not page finance.
| File | Command / Code | Purpose |
|---|---|---|
| Dockerfile.copy-default | FROM python:3.12-slim AS base | Rule 1 |
| Dockerfile.add-remote | ADD --checksum=sha256:270d731bd08040c6a3228115de1f74b91cf441c584139ff8f8f6503447... | Rule 2 |
| Dockerfile.bind-mounts | FROM python:3.12-slim | Rule 4 |
Key takeaways
Common mistakes to avoid
4 patternsUsing ADD for plain local files out of habit
ADD of an unpinned remote URL
COPY . . without a strict .dockerignore
Fixing ownership with a follow-up RUN chown
Interview Questions on This Topic
What's the functional difference between COPY and ADD?
Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Notes here come from systems that actually shipped.
That's Docker. Mark it forged?
3 min read · try the examples if you haven't