Definition
- COPY – copies files and directories from the build context into the image exactly as they are; does one predictable thing.
- ADD – does everything COPY does, plus auto-extracts recognised local compressed archives (tar.gz, bz2, xz) and can fetch files from remote URLs.
- Preferred – COPY is the default because it is explicit and predictable; no risk of accidental extraction or unverified downloads.
- ADD use case – reserved for the narrow case of deliberately extracting a local archive in one step.
Think of two mailroom clerks at the National Bank of India. COPY is the careful clerk who takes exactly the documents you hand him and places them on the desk, unchanged. ADD is the over-eager clerk who might open a sealed box and spread its contents (auto-extract) or go out and fetch a parcel from an unknown sender (URL fetch). For a bank that values predictability and security, you want the careful clerk by default.
Understanding the Concepts
What do COPY and ADD have in common?
Both instructions take a source from the build context and a destination inside the image. For a plain local file, they behave identically.
# Both do the same thing for a plain file:
COPY target/transfer-service.jar /app/app.jar
ADD target/transfer-service.jar /app/app.jar
# Result: /app/app.jar is the JAR file in both cases.
How does ADD's auto-extraction work?
If the source is a recognised local archive, ADD automatically unpacks it into the destination. COPY does not.
# ADD: auto-extracts the tarball
ADD config-files.tar.gz /app/config/
# Result: /app/config/ contains the extracted files from the archive.
# COPY: places the tarball as-is
COPY config-files.tar.gz /app/config/
# Result: /app/config/config-files.tar.gz (the archive file itself).
This auto-extraction is occasionally convenient but often surprising. A reader cannot tell from the Dockerfile line alone whether a file or an unpacked tree ends up in the image.
How does ADD's URL fetch work?
ADD can download a file from a remote URL into the image. COPY cannot.
# ADD from URL: downloads the file
ADD https://example.com/driver.jar /app/lib/driver.jar
# The file is downloaded during build.
# COPY from URL: does not work
# COPY https://example.com/driver.jar /app/lib/driver.jar
# Error: source can't be a URL
This is discouraged because the download is not well-cached, there is no inline checksum verification, and it bakes a network dependency into the build.
How do COPY and ADD compare?
| Behavior | COPY | ADD |
|---|---|---|
| Copy local files | Yes | Yes |
| Auto-extract archives | No | Yes (tar.gz, bz2, xz) |
| Fetch from URL | No | Yes |
| Predictability | High (one behavior) | Low (hidden behaviors) |
| Security | Safe | URL fetch = supply-chain risk |
| Layer caching | Clean | URL fetch = unpredictable |
| Recommended | Default for all use cases | Only for intentional extraction |
Why is COPY preferred?
COPY does exactly one predictable thing. Every COPY line in a Dockerfile is unambiguous about what ends up in the image. The official Docker best practices explicitly recommend COPY unless you specifically need ADD's auto-extraction.
# CORRECT: COPY for everything by default
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY target/transfer-service.jar app.jar
COPY config/ config/
ENTRYPOINT ["java", "-jar", "app.jar"]
When is ADD the right choice?
ADD is justified only when you have a local archive that you deliberately want extracted in one step.
# ADD is justified here: intentional extraction of a local tarball
FROM eclipse-temurin:21-jre
WORKDIR /app
ADD bank-config.tar.gz /app/config/ # Deliberate extraction
COPY target/transfer-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
How should you handle remote files instead of ADD?
Use an explicit download in a RUN step with checksum verification.
# WRONG: ADD from URL with no verification.
ADD https://mirror.bank.local/drivers/postgres-42.7.3.jar /app/lib/
# No checksum. If the mirror is compromised, the image is compromised.
# CORRECT: explicit download with checksum verification.
RUN curl -fsSL -o /app/lib/postgres-42.7.3.jar \
https://mirror.bank.local/drivers/postgres-42.7.3.jar && \
echo "a1b2c3d4e5f6... /app/lib/postgres-42.7.3.jar" | sha256sum -c
# If the checksum fails, the build fails. Provenance is auditable.
What are common mistakes?
# WRONG: using ADD for a plain file copy.
# A reviewer cannot tell if this will auto-extract or not.
ADD application.properties /app/
# What if someone renames it to application.tar.gz? Surprise extraction!
# CORRECT: use COPY for plain files.
COPY application.properties /app/
# Always copies the file as-is. No surprises.
# WRONG: ADD from a URL without verification.
ADD https://untrusted-mirror.com/lib.jar /app/
# Supply-chain risk: the mirror could serve a malicious file.
# CORRECT: download and verify checksum.
RUN curl -fsSL -o /app/lib.jar https://mirror.bank.local/lib.jar && \
echo "expected-sha256-hash /app/lib.jar" | sha256sum -c
What should a senior engineer know?
- Use a linter like hadolint to flag ADD-from-URL in Dockerfile reviews.
- In a reviewed banking Dockerfile, every line should do one obvious, verifiable thing. COPY honours that.
- ADD's auto-extraction only works for local archives, not for URLs (URL-fetched archives are NOT extracted).
- COPY participates cleanly in layer caching, while ADD from a URL can behave unpredictably across builds.
# hadolint: lint the Dockerfile for best practices
hadolint Dockerfile
# Warning: DL3020 Use COPY instead of ADD for files/folders
# Warning: DL3003 Use COPY instead of ADD for remote URLs
Visual Overview
COPY (preferred default): ┌──────────────┐ ┌──────────────┐ │ Build Context │ ────▶ │ Image Layer │ │ file.jar │ COPY │ file.jar │ │ config.tar.gz │ ────▶ │ config.tar.gz│ ← File as-is └──────────────┘ └──────────────┘ ADD (use only for intentional extraction): ┌──────────────┐ ┌──────────────┐ │ Build Context │ ────▶ │ Image Layer │ │ config.tar.gz │ ADD │ config/ │ ← Extracted! │ │ │ app.yml │ │ │ │ db.yml │ └──────────────┘ └──────────────┘ Remote files (do NOT use ADD): ┌──────────────┐ │ ADD URL │ ← No checksum, supply-chain risk! └──────────────┘ ┌──────────────┐ │ RUN curl + │ ← Explicit download + sha256sum │ sha256sum │ Auditable, verifiable └──────────────┘
Key Takeaway
COPY and ADD both copy files from the build context into the image, and for a plain local file they behave identically. The difference is that ADD has two extra behaviors: it auto-extracts recognised local archives into the destination, and it can fetch files from remote URLs. COPY does neither; it copies files exactly as-is.
COPY is preferred as the default because it does one predictable thing. A reader can tell from a COPY line exactly what ends up in the image, with no risk of accidental extraction and no silent network fetch. ADD is reserved for the narrow case of deliberately extracting a local archive, and remote files should use an explicit curl with checksum verification in a RUN step.
The security angle makes this strict in banking. ADD from a URL fetches a file with no inline verification, so a compromised mirror could silently alter the image. An explicit download with sha256sum closes that gap and makes provenance auditable.
Remember one sentence: use COPY for everything by default, and reserve ADD only for intentional local archive extraction.
---