Docker 15 🐳 Managing Files inside Dockerfiles: COPY vs ADD (Auto-extraction & Remote URLs)
COPY and ADD are the two Dockerfile instructions that bring files from the build context into the image. They look interchangeable, and for the simple case of copying a local file they behave identically. The difference is that ADD does two extra things: it auto-extracts local tar archives and it can fetch remote URLs. Those extra behaviors are the reason the official guidance is to use COPY by default. COPY does one predictable thing, and a reviewer reading the Dockerfile can see exactly what will happen without knowing the file extension of the source.
The trap with ADD is the implicit behavior. ADD app.tar.gz /opt/ leaves the contents of the archive at /opt/, while COPY app.tar.gz /opt/ leaves the archive itself at /opt/app.tar.gz. If a build suddenly changes behavior because someone renamed an artifact, this is why. Understanding when each instruction is appropriate is a matter of knowing what you want the build to do and making that intent explicit.
This chapter covers the build context, COPY and its flags, ADD and its two extra behaviors, the auto-extraction rules, remote URL handling, and the patterns that keep file management predictable and secure.
Key point: Use COPY for nearly everything. Use ADD only when you deliberately want local tar auto-extraction (the classic case is ADD rootfs.tar.xz / when building a base image) or a verified remote download. Avoid ADD <url> for unverified downloads; curl or wget in a RUN instruction gives you checksum verification and cleanup in the same layer.
Why COPY and ADD both exist
The predictability problem. COPY was introduced as an additional instruction because ADD‘s auto-extraction behavior was causing surprises. A user who wanted to copy a compressed file into the image without extracting it would find that ADD had already unpacked it. COPY was introduced to provide a straightforward, predictable copy that does exactly what it says.
The build-context problem. Both instructions copy from the build context, which is the set of files accessible to the Docker daemon during the build. The build context is sent to the daemon when docker build runs, and COPY and ADD operate within that scope. Neither can reach outside the context, which is a deliberate security boundary.
The remote-artifact problem. Some builds need to download a file from the internet. ADD supports this directly. But the download is cached based on the URL, not the content, and ADD does not support authentication on older syntax. For anything more than a simple, versioned, checksum-verified download, RUN curl or RUN wget is a better tool.
The layer-hygiene problem. A remote download via ADD creates a layer that contains the downloaded file. If the file is later deleted, the layer still contains it, and the image size includes it. A RUN curl ... && rm ... does the download, verification, extraction, and cleanup in a single layer, so nothing unnecessary remains in the image.
The cache-precision problem. Modern BuildKit supports ADD --checksum=sha256:... for remote URLs, which binds the download to a specific checksum and improves cache precision. This closes part of the verification gap, though the layer-hygiene argument for RUN curl still stands when cleanup is needed.
a. The build context
Before comparing the instructions, the build context must be understood. When docker build is run, Docker sends the contents of the specified directory (and its subdirectories) to the Docker daemon. That set of files is the build context, and it is the only place COPY and ADD can read from.
The .dockerignore file controls what is excluded from the context. Files like .git, node_modules, and local .env files should be listed there so they never enter the context at all. This reduces the context size and prevents accidental inclusion of files that should not be in the image.
# .dockerignore
.git
node_modules
.env
Dockerfile
b. COPY: predictable file transfer
The COPY instruction copies files and directories from the build context into the image. It has no other behavior.
COPY ./app /usr/src/app
COPY requirements.txt /usr/src/app/
COPY does not extract archives. If the source is a .tar.gz, the archive is copied as-is. COPY does not fetch remote URLs. If the source is a URL, the build fails. This is the point: the instruction does one thing, and the result is predictable from reading the Dockerfile.
COPY supports flags that are important for efficient builds:
| Flag | Purpose |
|---|---|
--from=<stage> | Copy from a previous build stage or image |
--chown=user:group | Set ownership at copy time |
--chmod=<perms> | Set permissions at copy time |
--link | Create a layer that can be reused with --cache-from |
--parents | Preserve parent directory structure |
The --from flag is the backbone of multi-stage builds. A compiled binary is built in a builder stage and copied into a minimal final stage, so the build tools never reach the final image.
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app
FROM alpine:3.19
COPY --from=builder /app /app
CMD ["/app"]
The --link flag improves cache reuse. When the destination directory is created with --link, the copied files are placed in an empty directory that becomes a layer, and that layer can be reused in subsequent builds even if previous layers have changed. This is useful for rebasing images and for multi-stage builds where a COPY --from would otherwise be invalidated.
The --parents flag preserves the parent directory structure of the source. COPY --parents ./x/a.txt ./y/a.txt /parents/ produces /parents/x/a.txt and /parents/y/a.txt rather than /parents/a.txt twice.
c. ADD: auto-extraction and remote URLs
ADD does everything COPY does, plus two behaviors that COPY does not have.
Auto-extraction of local tar archives. When the source is a local tar archive, ADD extracts it into the destination directory. The recognized formats are .tar, .tar.gz, .tgz, .tar.bz2, .tbz2, .tar.xz, and .txz. A .zip file or a plain .gz file is not extracted; only tar formats are recognized.
ADD app.tar.gz /app/
# The contents of app.tar.gz are extracted into /app/
The classic use case is building a base image from a root filesystem tarball:
FROM scratch
ADD ubuntu-noble-core-cloudimg-amd64-root.tar.gz /
The --unpack flag controls the behavior explicitly. ADD --unpack=false app.tar.gz /app/ copies the archive without extracting it, which is useful when the implicit behavior is not wanted.
Remote URL download. When the source is a URL, ADD downloads the file to the destination. The file is not extracted, even if it is a tar archive. Remote archives are downloaded without unpacking unless --unpack=true is specified.
ADD https://example.com/tool.tar.gz /tmp/tool.tar.gz
# Downloads the file; does not extract
The download is cached based on the URL. If the URL does not change, Docker may use the cached layer even if the content at the URL has changed. For reproducible builds, pin the URL to a versioned artifact and use --checksum.
ADD --checksum=sha256:270d731bd08040c6a3228115de1f74b91cf441c584139ff8f8f6503447cebdbb \
https://example.com/app.tar.gz /tmp/app.tar.gz
ADD does not support authentication. If the URL requires credentials, RUN curl or RUN wget must be used, or the BuildKit secrets HTTP_AUTH_HEADER_<host> and HTTP_AUTH_TOKEN_<host> can be configured.
d. Why ADD for remote URLs is usually the wrong tool
The Docker documentation recommends using RUN curl or RUN wget instead of ADD for remote downloads, and the reasons are specific.
Layer bloat. The downloaded file lands in its own layer. If it is later deleted, the layer still contains it, so the image size includes the file even though it is not present in the final filesystem. A RUN curl ... && rm ... does the download and cleanup in one layer, so nothing unnecessary remains.
Verification gap. Classic ADD <url> syntax does not support checksum verification. A RUN curl can verify a SHA256 checksum in the same instruction:
RUN curl -fsSL https://example.com/tool.tar.gz -o /tmp/tool.tar.gz \
&& echo "9f2c8b1d... /tmp/tool.tar.gz" | sha256sum -c - \
&& tar -xzf /tmp/tool.tar.gz -C /usr/local && rm /tmp/tool.tar.gz
Modern BuildKit adds ADD --checksum=sha256:..., which closes the verification gap, but the layer-hygiene argument still applies when cleanup is needed.
Cache unpredictability. The cache is keyed on the URL, not the content. If the remote file is updated but the URL stays the same, Docker may keep using the old cached version. A versioned URL and --checksum improve this, but RUN curl with an explicit checksum gives full control.
Authentication. ADD does not support HTTP authentication. Any download behind a login or a private registry requires RUN curl or RUN wget.
e. Choosing between COPY and ADD
The decision is straightforward once the behaviors are separated.
| Behavior | COPY | ADD |
|---|---|---|
| Copy local files and directories | Yes | Yes |
| Auto-extract local tar archives | No | Yes |
| Fetch remote URL | No | Yes |
| Support authentication | No | No |
Support --checksum | No | Yes (BuildKit) |
| Predictable for a reviewer | Yes | Depends on file extension |
Use COPY for nearly everything. Use ADD only when you deliberately want one of its two extra behaviors.
| Situation | Recommendation |
|---|---|
| Copying application source | COPY |
| Copying configuration files | COPY |
| Building a base image from a rootfs tarball | ADD with the tarball |
| Extracting a local application bundle | ADD if extraction is intended |
| Downloading a versioned, checksum-verified artifact | ADD --checksum=sha256:... |
| Downloading with authentication | RUN curl or RUN wget |
| Downloading and cleaning up in one layer | RUN curl or RUN wget |
| Copying from a previous build stage | COPY --from= |
The default should be COPY. When ADD is used, the reason should be obvious from the instruction, and a comment should explain it when the reason is not.
f. Flags shared by both
Both COPY and ADD support flags that reduce layers and improve cache behavior.
--chown=user:group sets ownership at copy time. Without it, a separate RUN chown -R would be needed, which duplicates the entire directory into a new layer.
COPY --chown=node:node . /app
--chmod=<perms> sets permissions at copy time. This is a BuildKit feature.
COPY --chmod=755 script.sh /usr/local/bin/script.sh
--from=<stage> copies from a previous build stage or another image. This is only meaningful for COPY in practice, though ADD technically supports it.
--link creates a layer that can be reused independently of previous layers. This is useful for rebasing and for improving cache reuse in multi-stage builds.
Complete Example Session
# ============================================
# PART 1: BASIC COPY
# ============================================
FROM alpine:3.19
WORKDIR /app
COPY app.sh /app/app.sh
CMD ["/app/app.sh"]
# ============================================
# PART 2: COPY WITH TRAILING SLASH
# ============================================
FROM alpine:3.19
COPY config.json /app/
# /app/config.json
# ============================================
# PART 3: ADD AUTO-EXTRACTION
# ============================================
FROM scratch
ADD rootfs.tar.gz /
# Contents of the archive are extracted to the root
# ============================================
# PART 4: ADD WITHOUT EXTRACTION
# ============================================
# syntax=docker/dockerfile:1
FROM alpine:3.19
ADD --unpack=false app.tar.gz /app/app.tar.gz
# Archive is copied, not extracted
# ============================================
# PART 5: ADD REMOTE URL
# ============================================
FROM alpine:3.19
ADD https://example.com/tool.tar.gz /tmp/tool.tar.gz
# Downloads the file; does not extract
# ============================================
# PART 6: ADD REMOTE URL WITH CHECKSUM
# ============================================
# syntax=docker/dockerfile:1
FROM alpine:3.19
ADD --checksum=sha256:270d731bd08040c6a3228115de1f74b91cf441c584139ff8f8f6503447cebdbb \
https://example.com/app.tar.gz /tmp/app.tar.gz
# ============================================
# PART 7: RUN CURL WITH VERIFICATION AND CLEANUP
# ============================================
FROM alpine:3.19
RUN apk add --no-cache curl && \
curl -fsSL https://example.com/tool.tar.gz -o /tmp/tool.tar.gz && \
echo "9f2c8b1d... /tmp/tool.tar.gz" | sha256sum -c - && \
tar -xzf /tmp/tool.tar.gz -C /usr/local && \
rm /tmp/tool.tar.gz
# ============================================
# PART 8: MULTI-STAGE WITH COPY --from
# ============================================
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app
FROM alpine:3.19
COPY --from=builder /app /app
CMD ["/app"]
# ============================================
# PART 9: COPY WITH --chown
# ============================================
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node . .
CMD ["node", "server.js"]
# ============================================
# PART 10: .dockerignore
# ============================================
# .dockerignore
.git
node_modules
.env
*.log
These ten parts cover basic COPY, COPY with a trailing slash, ADD auto-extraction, ADD without extraction, ADD with a remote URL, ADD with a checksum, RUN curl with verification and cleanup, multi-stage COPY --from, COPY --chown, and .dockerignore.
Quick Reference
COPY vs ADD
| Feature | COPY | ADD |
|---|---|---|
| Copy local files | Yes | Yes |
| Auto-extract local tar | No | Yes |
| Fetch remote URL | No | Yes |
| Support authentication | No | No |
Support --checksum | No | Yes (BuildKit) |
| Predictable | Yes | Depends on file extension |
Common Flags
| Flag | Purpose |
|---|---|
--from=<stage> | Copy from a build stage or image |
--chown=user:group | Set ownership at copy time |
--chmod=<perms> | Set permissions at copy time |
--link | Reusable layer for cache |
--parents | Preserve parent directory structure |
--unpack=<bool> | Control tar extraction (ADD) |
--checksum=sha256:... | Verify remote download (ADD) |
When to Use Each
| Situation | Instruction |
|---|---|
| Copy source code | COPY |
| Copy config files | COPY |
| Build base image from rootfs tarball | ADD |
| Extract a local bundle | ADD (if intended) |
| Download versioned artifact | ADD --checksum |
| Download with authentication | RUN curl |
| Download and clean up in one layer | RUN curl |
| Copy from a build stage | COPY --from |
Trailing Slash Behavior
| Instruction | Result |
|---|---|
COPY file /dest | /dest is a file |
COPY file /dest/ | /dest/file is created |
Best Practices
✅ Do This:
# Use COPY for local files
COPY . /app
# Use COPY --from for multi-stage builds
COPY --from=builder /app /app
# Use ADD only for tar extraction
ADD rootfs.tar.gz /
# Use RUN curl for remote downloads with verification
RUN curl -fsSL https://example.com/tool.tar.gz -o /tmp/t.tgz \
&& echo "9f2c8b1d... /tmp/t.tgz" | sha256sum -c - \
&& tar -xzf /tmp/t.tgz -C /usr/local && rm /tmp/t.tgz
# Use --chown to set ownership at copy time
COPY --chown=node:node . /app
❌ Don’t Do This:
# Use ADD for plain file copies
ADD package.json /app/ # ❌ use COPY
# Use ADD <url> without checksum
ADD https://example.com/tool.tar.gz /tmp/ # ❌ unverified
# Use ADD <url> when authentication is needed
ADD https://private.example.com/tool.tar.gz /tmp/ # ❌ unsupported
# Rely on ADD's implicit extraction
ADD app.tar.gz /app/ # ❌ if you wanted the archive, not its contents
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Archive unexpectedly extracted | Used ADD with a tar file | Use COPY or --unpack=false |
| Remote download not verified | Used ADD <url> | Use --checksum or RUN curl |
| Layer bloat from download | File remains in layer after deletion | Use RUN curl && rm |
| Authentication fails | ADD does not support auth | Use RUN curl |
| Cache uses stale remote file | URL unchanged, content changed | Use versioned URL and --checksum |
| Copy fails from outside context | Build context boundary | Move file into context or use --from |
| Trailing slash confusion | Destination treated as file vs directory | Always use trailing slash for directories |
Real-World Examples
1. Copy Source Code
COPY . /app
2. Copy Single File
COPY package.json /app/package.json
3. Copy from Builder Stage
COPY --from=builder /app /app
4. Extract Rootfs Tarball
FROM scratch
ADD rootfs.tar.gz /
5. Extract Application Bundle
ADD dist.tar.gz /app/
6. Download with Checksum
ADD --checksum=sha256:... https://example.com/app.tar.gz /tmp/app.tar.gz
7. Download with RUN and Cleanup
RUN curl -fsSL https://example.com/app.tar.gz | tar -xz -C /app
8. Set Ownership at Copy
COPY --chown=node:node . /app
9. Preserve Parent Directories
COPY --parents ./src/a.txt ./src/b.txt /app/
10. Reusable Layer with –link
COPY --link /app /app
Visual
COPY vs ADD Behavior
┌──────────────────────────────────────────────────────────────┐
│ COPY app.tar.gz /opt/ │
│ └── /opt/app.tar.gz (archive as-is) │
│ │
│ ADD app.tar.gz /opt/ │
│ └── /opt/ (contents extracted) │
│ │
│ COPY config.json /opt/ │
│ └── /opt/config.json (file) │
│ │
│ ADD https://example.com/tool.tar.gz /opt/ │
│ └── /opt/tool.tar.gz (downloaded, not extracted) │
└──────────────────────────────────────────────────────────────┘
Decision Flow
┌──────────────────────────────────────────────────────────────┐
│ What are you copying? │
│ │ │
│ ├── Local files/directories ──▶ COPY │
│ │ │
│ ├── Local tar archive + want extraction ──▶ ADD │
│ │ │
│ ├── Remote URL + want verification ──▶ ADD --checksum │
│ │ │
│ ├── Remote URL + need auth ──▶ RUN curl │
│ │ │
│ └── Remote URL + want cleanup ──▶ RUN curl && rm │
└──────────────────────────────────────────────────────────────┘
Layer Hygiene with Remote Download
┌──────────────────────────────────────────────────────────────┐
│ ADD https://example.com/tool.tar.gz /tmp/tool.tar.gz │
│ └── Layer contains the archive │
│ └── Deleting it later does not remove it from the layer │
│ │
│ RUN curl -fsSL https://example.com/tool.tar.gz -o /tmp/t.tgz \
│ && tar -xzf /tmp/t.tgz -C /usr/local && rm /tmp/t.tgz │
│ └── One layer: download, extract, cleanup │
│ └── No archive remains in the image │
└──────────────────────────────────────────────────────────────┘
Multi-Stage with COPY –from
┌──────────────────────────────────────────────────────────────┐
│ STAGE 1: builder │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ FROM golang:1.22 │ │
│ │ COPY . . │ │
│ │ RUN go build -o /app │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ │ COPY --from=builder /app /app │
│ ▼ │
│ STAGE 2: final │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ FROM alpine:3.19 │ │
│ │ COPY --from=builder /app /app │ │
│ │ CMD ["/app"] │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Build tools never reach the final image. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
COPY | Copies files and directories from build context |
ADD | COPY plus local tar extraction and remote URLs |
| Auto-extraction | ADD only; tar formats only |
| Remote URL | ADD only; no auth, no extraction by default |
--checksum | ADD with BuildKit; verifies remote download |
--unpack | ADD; controls tar extraction |
--from | Copy from a previous build stage |
--chown | Set ownership at copy time |
--link | Reusable layer for cache |
| Default recommendation | Use COPY for nearly everything |
Key takeaways:
COPYdoes one predictable thing: it copies files and directories. It does not extract archives, does not fetch URLs, and its behavior is obvious from the Dockerfile.ADDdoes everythingCOPYdoes, plus two extra behaviors. It auto-extracts local tar archives and can fetch remote URLs. The auto-extraction is implicit and depends on the file extension, which is why it surprises people.- Use
COPYfor nearly everything. The default should beCOPY. UseADDonly when you deliberately want tar extraction or a verified remote download. - Avoid
ADD <url>for unverified downloads. The download lands in its own layer and remains in the image even if deleted. UseRUN curlorRUN wgetfor authentication, checksum verification, and cleanup in one layer. - Modern BuildKit adds
ADD --checksum=sha256:.... This closes the verification gap for remote URLs, but the layer-hygiene argument still applies when cleanup is needed. COPY --fromis the backbone of multi-stage builds. It copies artifacts from a builder stage into a minimal final stage, so build tools never reach the final image.- Use
--chownto set ownership at copy time. Without it, a separateRUN chownduplicates the directory into a new layer. - The build context is a security boundary.
COPYandADDcannot reach outside the context. Use.dockerignoreto exclude files that should not enter the context at all.
Remember: COPY and ADD both bring files into the image, but ADD has two extra behaviors that make it less predictable. The official guidance is to use COPY by default because it does exactly what it says. Use ADD only when you want local tar auto-extraction or a checksum-verified remote download, and use RUN curl or RUN wget when you need authentication, verification, or cleanup in a single layer. The build context is the scope for both instructions, and .dockerignore controls what is available. Understanding these differences is what keeps Dockerfiles predictable, secure, and small.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!