Skip to main content

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=stage flags identically.
  • The Docker Dockerfile best-practices guide says: "Although ADD and COPY are functionally similar, generally speaking, COPY is preferred."
  • Reach for ADD only when you specifically need its tar or URL feature.

Quick example

dockerfile
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

FeatureCOPYADD
Local file/dir copyYesYes
--chown=user:group flagYesYes
--chmod=755 flagYesYes
--from=stage (multi-stage)YesYes
Auto-extract local tarNoYes
Fetch from URLNoYes
Predictable, easy to readYesNo (depends on input)
Recommended defaultYesNo

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

dockerfile
# 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

dockerfile
# 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.deb

The 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

dockerfile
# 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 COPY exclusively. The ADD lines you see are usually ADD some-rootfs.tar.gz / in FROM scratch minimal images.
  • Distroless and Alpine base images themselves are built with ADD <rootfs>.tar.gz / because that is exactly what ADD was designed for.
  • Google Cloud Build, GitHub Actions Dockerfile linters flag ADD <URL> and recommend the RUN curl pattern.

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

dockerfile
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

dockerfile
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 ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Comments

No comments yet