COPY and ADD in Docker: Two Ways to Get Files Into an Image, and Why They're Not the Same
Almost every Dockerfile ever written contains at least one line that moves a file from the outside world into the image being built. Two instructions can do this job — COPY and ADD — and picking the wrong one out of habit is one of the most common, and most avoidable, sources of bloated, unpredictable images.
Imagine two ways to get a piece of furniture into a new apartment. The first way is simple and predictable: you personally carry a specific box from a specific shelf in your garage and place it exactly where you want it in the new home — nothing more, nothing less. The second way is more like hiring a delivery service that will not only carry local boxes for you, but will also, if you hand it a receipt instead of a box, drive to the store, buy the item, unbox it, and set it up for you automatically. Both methods get furniture into the apartment. One is simple and does exactly what it says; the other is more powerful, but that extra power comes with surprises if you didn’t expect it. This is essentially the relationship between Docker’s two file-transfer instructions: COPY, the simple and predictable option, and ADD, the more powerful option with extra built-in behaviors that catch many engineers off guard. This article explains exactly what each instruction does, why Docker’s own documentation recommends one over the other in almost every situation, and the specific, narrow cases where the more powerful option actually earns its keep.
1Core Concepts
Before comparing the two instructions, it helps to understand the basic problem both of them exist to solve: getting files from outside a container image into the filesystem that image will contain.
Why a Dockerfile Needs to Move Files At All
A Docker image starts, in most cases, from a base image containing an operating system and perhaps a language runtime, but no application code. Somewhere in the process of building a custom image, the actual application source code, configuration files, or static assets have to make their way from the machine performing the build into the image’s own filesystem layers. This is a fundamentally different problem than running a command inside the image, because the files being copied usually live outside the container entirely, sitting on the disk of whichever machine is running docker build.
What COPY Does
The COPY instruction does exactly one job: it copies files or directories from the build context — the folder you pointed docker build at — into a specified location inside the image being built. It is a direct, literal file copy operation, comparable to dragging a file from one folder to another on your own computer. It does not fetch anything from the internet, and it does not automatically unpack compressed archives.
COPY is like a librarian who, when asked to place a specific book on a specific shelf, does precisely that and nothing else. Hand the librarian a sealed box, and it goes on the shelf exactly as a sealed box — the librarian doesn’t decide to open it, and doesn’t go check whether a newer edition exists somewhere else. What you hand over is exactly what ends up in place.
What ADD Does
The ADD instruction can do everything COPY does, plus two additional, automatic behaviors: if the source is a recognized compressed archive format (such as a tar file, optionally gzip or bzip2 compressed) located in the build context, ADD automatically extracts its contents into the destination directory instead of copying the archive file itself; and if the source is a URL rather than a local file, ADD will download that remote file and place it in the image.
COPY does one predictable thing: copy local files as-is. ADD does that same thing, plus automatic archive extraction and remote URL fetching — extra convenience that comes with extra, often unwanted, surprises.
Why This Distinction Matters From Day One
Because ADD’s extra behaviors are triggered automatically based on what the source looks like, rather than through an explicit flag the Dockerfile author chooses, its behavior can be harder to predict at a glance. A teammate reading a Dockerfile with a COPY instruction knows immediately and unambiguously what will happen. A teammate reading an ADD instruction has to first check whether the source is a plain file, an archive, or a URL, because each one leads to meaningfully different behavior in the final image.
2Architecture & Components
Both instructions sit inside Docker’s broader build architecture, particularly the concept of the build context and the layered filesystem that every image is made of.
The Build Context
When a Dockerfile is built, Docker first gathers every file in a specified directory — commonly the current folder — into something called the build context, and sends that entire context to the build engine before a single instruction runs. Both COPY and ADD can only reference files that exist inside this build context; neither instruction can reach outside of it to grab an arbitrary file from elsewhere on the host machine’s filesystem, which is a deliberate security boundary preventing a Dockerfile from accidentally, or maliciously, pulling in unrelated files it was never meant to see.
flowchart LR
A["Host Machine Folder
(the build context)"] -->|docker build .| B["Build Context
Sent to Build Engine"]
B --> C{"Dockerfile Instruction"}
C -->|COPY src dest| D["Direct file copy
into new image layer"]
C -->|"ADD src.tar.gz dest"| E["Automatic extraction
into new image layer"]
C -->|"ADD https://... dest"| F["Remote download
into new image layer"]
D --> G["Final Image Filesystem"]
E --> G
F --> G
Fig 2.1 — Both instructions pull from the same build context, but ADD branches into extra automatic behaviors based on the source type
Layers and Image Size
Every COPY or ADD instruction creates a new, immutable layer in the resulting image. Understanding this matters directly for image size and build speed: if a large file is copied in an early layer and later removed in a subsequent instruction, the bytes from that file are still physically present in the earlier layer and still count toward the final image’s total size, because layers are additive and earlier ones are never silently rewritten by later ones.
COPY
Direct, literal copy of files or directories from the build context into the image. No extraction, no network access.
ADD (local archive)
Automatically detects and extracts recognized compressed archive formats from the build context into the destination.
ADD (remote URL)
Downloads a file from a URL directly into the image during the build, without needing a separate download step.
Build Context Boundary
Neither instruction can reach files outside the build context passed to docker build.
Ownership and Permissions
Both instructions support an option to set the ownership (user and group) of the copied files as they land inside the image, which matters a great deal for images designed to run as a non-root user — a very common security best practice. Without explicitly setting ownership, copied files typically land owned by the root user by default, which can cause a permission error later if the container’s main process is deliberately configured to run as a different, non-root user.
3Internal Working
Understanding exactly what happens, step by step, during each type of copy operation clarifies why the two instructions behave so differently in practice.
How COPY Processes a Request
When the build engine reaches a COPY instruction, it looks up the specified source path or paths inside the already-collected build context, verifies they exist, and writes their exact bytes into the destination path inside a new image layer. There is no interpretation of file type, no decision-making about whether something looks like an archive, and no network activity of any kind. What goes in is exactly what comes out, one-to-one.
How ADD Processes a Local Archive
When ADD encounters a source file in the build context that matches a recognized compressed archive format, it does not simply copy that archive file into the destination as a single compressed file. Instead, it automatically unpacks the archive’s contents directly into the destination directory, as though someone had manually extracted it and then copied each resulting file over individually. This automatic extraction only applies to local files in the build context — if the exact same archive were instead placed at a remote URL, ADD’s URL-handling path does not perform this same automatic extraction, which is a subtle but important asymmetry to remember.
ADD’s automatic extraction behavior applies specifically to local archives from the build context. A remote URL pointing at an archive file is simply downloaded as-is, as a single compressed file, without being automatically unpacked. Many engineers assume the two cases behave identically, and are surprised to find a compressed file sitting untouched inside their image after using ADD with a URL.
How ADD Processes a Remote URL
When the source given to ADD is a URL rather than a local path, the build engine performs an HTTP request during the image build itself, downloads the resulting file, and places it at the specified destination inside the new layer. This means the network has to be reachable at build time for this step to succeed, and the exact bytes downloaded depend entirely on what that remote URL happens to serve back at the moment the build runs — which may not be the same content every time, especially if the URL points at something that changes over time.
sequenceDiagram
participant Ctx as Build Context
participant Build as Build Engine
participant Net as Remote Server
participant Img as New Image Layer
Build->>Ctx: Read local source for COPY
Ctx-->>Build: Return raw file bytes
Build->>Img: Write bytes unchanged
Note over Build,Img: COPY: no branching logic
Build->>Ctx: Read local source for ADD
alt Source is a recognized archive
Ctx-->>Build: Return archive bytes
Build->>Build: Extract archive contents
Build->>Img: Write extracted files
else Source is a URL
Build->>Net: HTTP GET request
Net-->>Build: Return remote file bytes
Build->>Img: Write downloaded file as-is
end
Fig 3.1 — COPY follows one straight path; ADD branches based on what kind of source it was given
Why Reproducibility Suffers With Remote ADD
Because a URL-based ADD performs a live network request at build time, the exact same Dockerfile can theoretically produce a different image on two separate build runs if the content at that URL changes in between — something that would never happen with a COPY instruction referencing a fixed, version-controlled local file, since the build context itself does not silently change between runs unless the developer deliberately changes it.
4Data Flow & Lifecycle
Tracing a file’s journey from a developer’s own repository all the way into a running container makes clear exactly where COPY and ADD sit in the broader build-and-deploy lifecycle.
Source Files Committed
Application code, configuration, and static assets live in version control, exactly as any team member can review and inspect them.
docker build Gathers the Context
The build command bundles the specified folder’s contents into the build context sent to the build engine.
COPY or ADD Executes
Files move from the context into a new image layer, with ADD potentially extracting an archive or fetching a URL along the way.
Layer Caching Decision
Docker checks whether the copied content changed since the last build to decide whether this layer, and every layer after it, can be reused from cache.
Image Published
The finished image, containing every copied file baked permanently into its layers, is pushed to a registry.
Container Runs With Baked-In Files
Every container started from the image already contains the copied files; no copy operation happens again at container run time.
Build Cache and Instruction Ordering
Docker’s build cache checks whether the content being copied has changed since the previous build; if it hasn’t, Docker can reuse the previously built layer instead of repeating the copy operation, which significantly speeds up repeated builds. This is precisely why experienced Dockerfile authors deliberately order their COPY instructions carefully — copying files that rarely change (such as a dependency manifest) before files that change constantly (such as application source code), so that a small source code edit doesn’t invalidate and force a rebuild of an expensive dependency installation step that didn’t actually need to change.
Real Pattern: Separating Dependency Files From Source Code
A common, high-value pattern copies only a project’s dependency manifest first, runs the dependency installation step, and only then copies the rest of the application source code in a later instruction. Because the dependency manifest changes far less often than application code, this ordering means routine code changes don’t force a full, slow dependency reinstall on every single build — only an actual dependency change does.
Where the URL-Fetching Behavior Breaks This Lifecycle
Because a URL-based ADD performs a live network call, its build cache behavior is comparatively less predictable than a local COPY — the cache generally keys off the instruction’s declared source, not off freshly re-checking the remote content’s current state on every single build, which means a changed remote file might not automatically trigger a fresh download unless the build cache is deliberately invalidated. This is one more reason experienced teams generally prefer downloading remote files in a separate, explicit, controlled step, rather than relying on ADD’s built-in URL behavior.
5Advantages, Disadvantages & Trade-offs
Neither instruction is universally “better” — each represents a different trade-off between simplicity and convenience.
COPY — Advantages
- Behavior is completely predictable: exactly what you specify is exactly what happens
- No hidden network calls, keeping builds reproducible and offline-friendly
- Easier for a reviewer to reason about at a glance, with no branching logic to consider
- Officially recommended default for the vast majority of Dockerfile authoring
ADD — Trade-offs
- Automatic archive extraction can be genuinely convenient for a small, specific set of use cases
- Remote URL fetching introduces a live network dependency into the build process itself
- Behavior depends on inferring the source type, making it less immediately obvious to a reader
- Can silently produce a larger or differently structured image than a reader expects
Why Official Guidance Leans Toward COPY
Docker’s own documentation explicitly recommends COPY for the majority of cases specifically because of its predictability: a Dockerfile is a piece of infrastructure that is often read, reviewed, and modified by people other than its original author, sometimes years later. An instruction whose behavior can be fully understood without needing to check the nature of its source file is significantly easier to maintain safely over that long lifespan than one requiring extra context to interpret correctly.
Where ADD’s Extra Power Genuinely Helps
The one case where ADD’s automatic archive extraction is broadly considered a legitimate, even preferred, choice is unpacking a local tarball of application files directly into an image in a single instruction, avoiding a separate manual extraction step elsewhere in the build. Outside of this specific case, and outside of a handful of ADD’s own documented, narrow local-archive scenarios, most style guides and linters actively recommend against reaching for ADD’s remote URL behavior in production Dockerfiles.
6Security
The extra automatic behaviors that make ADD more powerful than COPY are precisely the same behaviors that introduce meaningful security considerations.
The Risk of Fetching Remote Content During a Build
Using ADD with a remote URL means the build process itself reaches out to a server outside of your control, at build time, and trusts whatever content that server returns. If that URL is ever compromised, redirected, or simply serves different content than expected, the resulting image can silently contain unexpected, potentially malicious content, without the Dockerfile itself ever visibly changing. This is a meaningfully larger trust surface than a COPY instruction referencing a file that already lives, reviewed and version-controlled, inside the same repository as the Dockerfile.
Using COPY is like packing your own suitcase yourself, item by item, so you know exactly what’s inside. Using ADD with a remote URL is like asking a stranger at the airport to hand you a sealed bag to check in as your own luggage — it might be exactly what you expect, but you have no way to verify that until it’s already gone through security and onto the plane.
Extraction Behavior and Archive Contents
Automatic extraction of an archive means every file packed inside that archive lands inside the image, whether or not the Dockerfile author fully audited every single file the archive contains. An archive built by an unfamiliar third-party tool or process could, in principle, include far more files than expected, and ADD’s automatic unpacking behavior means those extra files land inside the image without any additional visible step in the Dockerfile drawing attention to them.
Why Explicit, Separate Download Steps Are Often Preferred
Rather than relying on ADD’s built-in URL fetching, many security-conscious teams prefer to perform any necessary downloads as an explicit, separate, auditable step — one that can include verifying a cryptographic checksum of the downloaded file against a known-good value before it is trusted and copied into the image. This extra verification step is something ADD’s built-in URL behavior does not perform automatically, meaning a Dockerfile relying on it alone has no built-in protection against a tampered or unexpectedly different remote file.
The Problem
A Dockerfile uses ADD with a remote URL to fetch a third-party installer or binary directly into the image, with no checksum verification anywhere in the process.
Why It Fails
If the remote URL is ever compromised, or if its content changes unexpectedly for any reason, the resulting image silently changes too, with nothing in the Dockerfile itself signaling that anything is different. Builds also become dependent on that external server’s availability and integrity indefinitely into the future.
Better Approach
Download the file as an explicit, separate step that verifies a known checksum before the file is trusted, then use COPY to bring the verified file into the image from the local build context, keeping the trust boundary clearly visible in the Dockerfile.
7Monitoring, Logging & Metrics
While COPY and ADD are build-time instructions with no runtime footprint of their own, their effects on image size, build reliability, and reproducibility are all worth actively tracking.
Tracking Image Size Contribution
Because every COPY or ADD instruction adds a new layer, teams that care about lean images regularly inspect which layers contribute the most to an image’s total size, often discovering that an early, forgotten COPY of an unnecessarily large file (such as an entire local dependency cache folder that should have been excluded) is quietly bloating every derived image built from that Dockerfile ever since.
Layer Size Auditing
Regularly reviewing which COPY or ADD layers contribute most to total image size catches accidental bloat early.
Build Failure Tracking
Monitoring build failures caused by unreachable remote URLs used with ADD highlights a hidden external dependency worth removing.
Cache Hit Rate
Tracking how often COPY layers are served from cache versus rebuilt reveals whether instruction ordering is actually optimized.
Reproducibility Checks
Comparing two builds of the same commit for byte-identical output surfaces any hidden non-determinism, often traced back to a remote ADD source.
A Real Pattern: Flagging Remote ADD Usage in CI
Some organizations configure a linting step in their continuous integration pipeline that specifically scans every Dockerfile for ADD instructions pointing at a URL, flagging them for manual review rather than allowing them to merge automatically. This turns a subtle, easy-to-miss pattern into a visible, actively monitored signal the team can consciously accept or reject on a case-by-case basis.
8Design Patterns & Anti-patterns
A handful of clear, well-established patterns have emerged from years of real-world Dockerfile authorship around exactly when to reach for COPY versus ADD.
Pattern: Default to COPY, Always
The single most widely agreed-upon pattern across the Docker community is to default to COPY for essentially every file transfer, and to reach for ADD only when one of its specific extra behaviors is genuinely needed and deliberately chosen, never out of habit or because an old tutorial happened to use it.
Pattern: Multi-Stage Builds Combined With COPY
In a multi-stage build, a special form of COPY can pull files not from the local build context, but directly from an earlier build stage — for example, copying only a compiled binary out of a large build-tooling stage into a small, final runtime stage. This pattern keeps the final image lean while still allowing the build process itself to use whatever large, heavy tools it needs in an earlier, discarded stage.
Pattern: Reserving ADD Strictly for Local Archive Extraction
When a project genuinely needs to unpack a local tarball as part of its build, using ADD for that single, specific purpose remains a reasonable, well-understood pattern — the key discipline is using it only for that purpose, and continuing to use COPY for every other file in the same Dockerfile, rather than switching entirely to ADD everywhere out of convenience.
The Problem
A team uses ADD everywhere in their Dockerfiles as a habit, on the belief that it is simply “the more capable version of COPY” and therefore always the safer default choice.
Why It Fails
This inverts the actual trade-off. ADD’s extra capabilities introduce extra, often unintended behavior — an innocuously named file that happens to match a recognized archive extension could be silently extracted instead of copied as-is, changing the final image’s file structure in a way nobody intended or noticed during review.
Better Approach
Default to COPY everywhere, and only switch to ADD for a specific instruction where its automatic extraction behavior is deliberately wanted, documenting why in a nearby comment so future readers understand the choice was intentional.
Anti-pattern: Fetching Application Dependencies With Remote ADD
Some Dockerfiles use ADD with a URL to fetch an entire application dependency, such as a specific tool binary, directly during the build. This bypasses whatever dependency management and version-locking practices a project otherwise follows elsewhere, and ties every future build’s success to that one remote URL remaining available and unchanged indefinitely.
Pattern: Explicit .dockerignore as a Companion to COPY
Because a broad COPY instruction naturally pulls in everything present in its source directory, mature projects pair it with an ignore file that explicitly lists paths to exclude from the build context entirely — things like local dependency caches, editor configuration folders, and version control metadata. This means a simple, broad COPY instruction stays safe and lean, because the unwanted files never even make it into the build context in the first place, rather than relying on the Dockerfile author to remember every exclusion at the moment they write each individual COPY line.
9Best Practices & Common Mistakes
Bringing everything together into a practical checklist for choosing and using these two instructions correctly.
| Practice | Why It Matters |
|---|---|
| Default to COPY unless a specific ADD behavior is genuinely needed | Keeps Dockerfile behavior predictable and easy to review |
| Copy dependency manifests before application source code | Maximizes build cache reuse on routine code changes |
| Avoid ADD with remote URLs in production Dockerfiles | Prevents a live, unverified network dependency at build time |
| Set explicit file ownership during COPY for non-root containers | Prevents permission errors when the process runs as a non-root user |
| Exclude unnecessary files from the build context | Prevents accidentally copying large, irrelevant files into the image |
| Document any intentional use of ADD with a short comment | Signals to future readers that the extra behavior was a deliberate choice |
Common Mistake: Copying an Entire Project Directory Blindly
A frequent mistake is copying an entire project folder into the image with a single broad instruction, without first excluding files that don’t belong in the image at all, such as local development configuration, version control metadata, or large local caches. This routinely bloats the resulting image and can even accidentally copy sensitive local files, such as credentials a developer keeps in an untracked local file, directly into a shareable image.
Common Mistake: Assuming ADD’s Extraction Always Applies
Some engineers mistakenly assume that ADD will extract any archive-like file regardless of where it came from, and are then confused when a file downloaded from a URL lands in the image still compressed. As covered earlier, automatic extraction is specific to local build-context sources, not remote URL sources, and forgetting this distinction leads to real, hard-to-diagnose build output surprises.
When reviewing an unfamiliar Dockerfile, treat any ADD instruction as a place worth pausing on specifically. Ask what the source actually is — a plain file, an archive, or a URL — because that single detail determines the real, and sometimes surprising, effect that instruction has on the final image.
10Real-world & Industry Examples
The COPY-versus-ADD decision shows up in real Dockerfiles across the industry, often reflecting exactly the trade-offs discussed throughout this article.
Example: Official Language Runtime Images
Many official language runtime image Dockerfiles use COPY almost exclusively for bringing local files into an image, reserving ADD only for the narrow, specific case of extracting a local archive containing a language runtime’s own source distribution during the base image’s own build process — a textbook example of ADD’s extraction behavior used exactly where it earns its keep, and nowhere else.
Example: Web Application Teams and Multi-Stage COPY
A typical web application at a mid-sized technology company builds its frontend assets in one build stage using a full set of build tools, then uses a stage-to-stage COPY to bring only the finished, compiled output into a lightweight final image based on a minimal web server. Neither ADD’s archive extraction nor its URL fetching plays any role here — the entire pattern is built entirely on COPY’s simple, predictable behavior applied cleverly across build stages.
Example: A Cautionary Tale From Public Advisories
Security researchers and container scanning vendors have documented real incidents where a Dockerfile’s reliance on ADD with a remote URL, pointed at a third-party hosting service, resulted in unexpectedly different content being fetched after the original file was moved or the hosting arrangement changed, silently altering images built from an otherwise unchanged Dockerfile. These documented cases are a large part of why official guidance and community linters alike steer engineers away from this specific pattern in production use.
Example: Legacy Codebases Still Using tar Archives
Some long-lived enterprise codebases package portions of their build output as a local tarball for historical reasons predating containerization, and continue to rely on ADD’s automatic local-archive extraction to unpack that tarball cleanly into an image during migration to Docker, rather than rewriting their entire build pipeline at once. This remains one of the clearest, most legitimate real-world uses of ADD’s extra behavior.
11Frequently Asked Questions
12Summary and Key Takeaways
COPY and ADD both move files into a Docker image, but the gap between “simple and predictable” and “powerful but surprising” is exactly what every Dockerfile author needs to keep in mind when choosing between them.
Key Takeaways
- COPY is a direct, literal file copy. What you specify as the source is exactly what lands in the destination, with no extraction and no network activity.
- ADD does everything COPY does, plus two extras. It automatically extracts recognized local archives, and can fetch a remote file when given a URL as its source.
- The extraction behavior is local-only. A URL pointing at an archive is downloaded as-is, without automatic unpacking — a frequently missed asymmetry.
- Predictability is why COPY is the recommended default. A Dockerfile that relies mainly on COPY is easier for any future reader to understand at a glance.
- Remote ADD introduces real risk. A live network fetch at build time trusts an external server’s content, with no built-in verification of what comes back.
- ADD still has a legitimate niche. Extracting a local, version-controlled archive in a single instruction remains a reasonable, well-understood use case.
- Both are strictly build-time instructions. Neither one does anything once a container built from the resulting image is actually running.