COPY vs ADD in Dockerfile: what is the difference?
COPY and ADD in Dockerfile look almost identical at first glance — both copy files from your build context into the image. The difference is what ADD does on top of copying.
Theory
TL;DR
COPY= plain file/directory copy. Predictable. Default choice.ADD=COPY+ auto-extract local tar archives + fetch from remote URLs.- Both support
--chown,--chmod,--from=stageflags identically. - The Docker Dockerfile best-practices guide says: "Although
ADDandCOPYare functionally similar, generally speaking,COPYis preferred." - Reach for
ADDonly when you specifically need its tar or URL feature.
Quick example
FROM alpine:3.21
# COPY: plain copy. What you see is what you get.
COPY package.json /app/
COPY src/ /app/src/
# ADD: copy + extract tar (tar.gz, tar.bz2, tar.xz, tar)
ADD app.tar.gz /app/
# ↑ The tarball is unpacked in /app/, NOT placed there as-is.
# ADD: also fetches URLs (works, but discouraged)
ADD https://example.com/file.deb /tmp/
# ↑ No checksum check, no caching, no retry control.Four lines, two surprises if you did not know ADD's extras.
Comparison table
| Feature | COPY | ADD |
|---|---|---|
| Local file/dir copy | Yes | Yes |
--chown=user:group flag | Yes | Yes |
--chmod=755 flag | Yes | Yes |
--from=stage (multi-stage) | Yes | Yes |
| Auto-extract local tar | No | Yes |
| Fetch from URL | No | Yes |
| Predictable, easy to read | Yes | No (depends on input) |
| Recommended default | Yes | No |
When ADD is actually the right call
- You have a local tarball you want unpacked in one step. Saves a
RUN tar xf ...line. - You are building from scratch and need to pull a base rootfs as a tar (
FROM scratch+ADD rootfs.tar.gz /).
For URL fetches, prefer a RUN curl ... && check-checksum && rm in a single RUN. You get explicit control over verification and can clean up in the same layer.
Common mistakes
Using ADD for plain copies because it sounds more powerful
# WRONG: surprises future readers
ADD package.json /app/
# RIGHT: plain copy with plain command
COPY package.json /app/ADD here is functionally the same as COPY, but a teammate reading the file has to wonder "is this file a tarball that gets extracted?" Reduce cognitive load. Use COPY.
Using ADD <URL> and trusting it for production
# WRONG: no checksum verification, no retry, leaves garbage
ADD https://example.com/binary.deb /tmp/
RUN dpkg -i /tmp/binary.deb
# RIGHT: explicit fetch with verification
RUN curl -fsSL -o /tmp/binary.deb https://example.com/binary.deb && \
echo "<sha256> /tmp/binary.deb" | sha256sum -c - && \
dpkg -i /tmp/binary.deb && \
rm /tmp/binary.debThe RUN version is verifiable, cleans up in the same layer, and fails loudly if the upstream binary changes silently.
Forgetting that ADD some.tar.gz / extracts
# Surprise: this does NOT put a tarball in /opt/
ADD vendor.tar.gz /opt/
# It extracts the contents into /opt/.
# If you wanted the file as-is, COPY it.A classic gotcha when migrating Dockerfiles.
Real-world usage
- Most production Dockerfiles use
COPYexclusively. TheADDlines you see are usuallyADD some-rootfs.tar.gz /inFROM scratchminimal images. - Distroless and Alpine base images themselves are built with
ADD <rootfs>.tar.gz /because that is exactly whatADDwas designed for. - Google Cloud Build, GitHub Actions Dockerfile linters flag
ADD <URL>and recommend theRUN curlpattern.
Follow-up questions
Q: If ADD does more, why is it not the default recommendation?
A: Because "more" means "less predictable". Reading COPY foo /bar, you know exactly what happens. Reading ADD foo /bar, you have to know the type of foo to know what happens. Boring and explicit beats clever.
Q: Does ADD extract .zip?
A: No. Only tar variants: .tar, .tar.gz (or .tgz), .tar.bz2, .tar.xz. For .zip, use COPY plus a RUN unzip.
Q: Can I use COPY with a URL?
A: Not directly. With BuildKit (# syntax=docker/dockerfile:1.7+), you can use ADD <URL> with checksum verification via --checksum=sha256:... — that is the modern, safe way to fetch URLs. Plain COPY remains local-only.
Q: (Senior) When would you intentionally pick ADD for a URL today?
A: With BuildKit's ADD --checksum=sha256:abc... https://example.com/file, which gives you a verifiable, cache-friendly download in one line. That is the only ADD <URL> pattern I would put in production.
Examples
The 99 percent case: COPY for everything
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
CMD ["node", "server.js"]No ADD. No surprises. Clear cache boundaries. This is what most production Dockerfiles look like.
Legitimate ADD: building a base image from a rootfs
FROM scratch
ADD alpine-minirootfs-3.21.0-x86_64.tar.gz /
CMD ["/bin/sh"]Here ADD does its real job: unpack a tar into the image. This is exactly how the official alpine image is built upstream.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.
Comments
No comments yet