COPY is almost always the command you want over ADD.
Let’s see why.
Here’s a super simple Dockerfile:
FROM alpine:latest
# This COPY command copies a local file into the image.
COPY hello.txt /app/hello.txt
# This ADD command also copies a local file into the image.
ADD goodbye.txt /app/goodbye.txt
CMD ["ls", "/app"]
And here are the files in our build context:
.
├── Dockerfile
├── hello.txt
└── goodbye.txt
hello.txt contains: Hello from COPY!
goodbye.txt contains: Goodbye from ADD!
When we build this, docker build -t copy-vs-add ., the output of the CMD will be:
hello.txt
goodbye.txt
Both commands seem to do the same thing: put local files into the image. But their behavior diverges significantly when you use URLs or tarballs.
ADD has two extra capabilities:
- It can fetch URLs.
- It can automatically extract compressed local archives (like
.tar,.gz,.zip).
Let’s look at the URL fetching. Suppose we replace the ADD line with:
ADD https://raw.githubusercontent.com/docker-library/docs/master/alpine/README.md /app/alpine_readme.md
Now, if we build, /app/alpine_readme.md will contain the content of Alpine’s README. COPY cannot do this. It will throw an error because it expects a local file path, not a URL.
The archive extraction is where things get particularly tricky. If goodbye.txt were actually archive.tar.gz containing a file named inside.txt, ADD archive.tar.gz /app/ would extract inside.txt directly into /app/. COPY archive.tar.gz /app/ would not extract it; it would simply copy the archive.tar.gz file itself into /app/.
This automatic extraction by ADD is often surprising. You might think you’re copying a file, but if it’s a tarball, it gets unpacked. This can lead to unexpected file structures in your image and potential security issues if you’re pulling in archives from untrusted sources.
The primary reason to use COPY is its predictability. It does one thing: copies files or directories from the build context into the image filesystem. It doesn’t guess your intent. It doesn’t unpack archives. It doesn’t fetch URLs. This makes your Dockerfile more explicit and easier to understand, especially in collaborative environments or when revisiting old builds.
When would you ever want ADD’s special features?
- Fetching external dependencies directly: If you need to pull a binary or a configuration file from a URL without an intermediate step.
- Simplifying archive handling: If you have a local tarball that you want unpacked directly into the image.
However, even for these cases, it’s often better practice to use RUN with tools like curl or wget for URL fetching, and RUN tar -xf for archive extraction. This makes the steps explicit and leverages the standard RUN instruction, which is designed for executing commands.
Consider this common scenario: you want to download a release artifact and put it in your image.
Using ADD:
FROM ubuntu:latest
ADD https://example.com/my-app-v1.0.tar.gz /tmp/
RUN tar -xzf /tmp/my-app-v1.0.tar.gz -C /opt/ && rm /tmp/my-app-v1.0.tar.gz
This seems simple, but the ADD command silently downloads and then you still need RUN to extract. If the URL was a tarball and you didn’t specify -C, ADD would extract it directly, potentially into the wrong place if you weren’t careful.
Using COPY with explicit RUN:
FROM ubuntu:latest
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
RUN curl -L https://example.com/my-app-v1.0.tar.gz -o /tmp/my-app-v1.0.tar.gz
RUN tar -xzf /tmp/my-app-v1.0.tar.gz -C /opt/ && rm /tmp/my-app-v1.0.tar.gz
This is more verbose, but it’s crystal clear what’s happening at each step. You’re installing curl, then downloading, then extracting. The intent is unambiguous.
The "magic" of ADD’s archive extraction is a footgun. It’s easy to accidentally unpack something you didn’t intend to, or to have a Dockerfile that behaves differently based on whether the source file is a tarball or not. Stick to COPY for moving files from your build context. If you need to download or unpack, use RUN.
The next time you encounter a layer that’s unexpectedly large or contains unpacked files you didn’t expect, check if ADD was used with a tarball.