RUN, COPY, and ADD
RUN, COPY, and ADD are the Dockerfile instructions that most often change the filesystem of an image. They matter because each one can create a layer, affect build speed, and decide exactly which files and tools end up inside the final image.
Use RUN to execute commands during the build, COPY to bring files from the build context into the image, and ADD only when you need its extra behavior. Good Dockerfiles are mostly about putting these three instructions in the right order.
Overview: How RUN, COPY, and ADD Work
When Docker builds an image, it starts from the image named by FROM and processes each later instruction from top to bottom. Image layers are read-only filesystem snapshots. A RUN instruction starts a temporary build container from the current image state, runs the command, captures the changed files, and stores those changes as a new layer. A COPY or ADD instruction copies files into the image and also records the result as a layer.
The source files for COPY and most ADD instructions come from the build context: the directory you pass at the end of docker build, usually .. Docker does not let a Dockerfile copy arbitrary files from your whole computer with COPY ../../secret.txt /app/; the files must be inside the context unless you use special build features outside this basic lesson. This boundary is intentional because the Docker client sends the context to the builder, which might be local, inside Docker Desktop’s Linux VM, or remote.
The cache is the other major idea. For each instruction, BuildKit checks whether it can reuse a previous result. For RUN, the command text and previous image state matter. For COPY and ADD, Docker also considers the copied file metadata and contents. If one instruction changes, Docker must rebuild that instruction and every instruction after it. That is why a Dockerfile that runs COPY . . before installing dependencies is slow: every source-code edit can invalidate the dependency install layer.
COPY is the plain, predictable file-copy instruction. ADD does everything COPY does, plus two special behaviors: it can automatically extract a local tar archive, and it can fetch a remote URL. Those extras are convenient but easy to misuse, so Docker’s common best practice is simple: prefer COPY unless you specifically need ADD‘s archive extraction or remote-source behavior.
Syntax
The common forms are:
RUN command
COPY source destination
COPY ["source with spaces", "destination/"]
ADD source destination
ADD https://example.com/file.txt /path/file.txt
| Instruction | Use it for | Important details |
|---|---|---|
RUN command |
Installing packages, creating users, compiling code, generating files during the build. | Runs at build time, not when the container starts. Usually creates a new filesystem layer. |
COPY source destination |
Copying application files from the build context into the image. | Predictable. Does not download URLs or auto-extract archives. |
COPY ["source", "destination"] |
Copying paths that contain spaces or need JSON-array clarity. | JSON form requires double quotes. |
ADD source destination |
Copying like COPY, extracting local tar archives, or fetching remote URLs. |
Local tar archives are extracted automatically. Prefer COPY for normal local files. |
Some useful options are supported by modern Docker builders. COPY --chown=user:group sets ownership while copying. COPY --chmod=755 sets permissions. These are cleaner than copying a file and then using a separate RUN chown or RUN chmod layer.
Examples
Example 1: RUN installs packages in one layer
This Dockerfile installs curl into a small Alpine-based image and removes the package index cache in the same RUN instruction:
FROM alpine:3.20
RUN apk add --no-cache curl
CMD ["curl", "--version"]
Build and run it:
docker build -t curl-tool:1.0 .
docker run --rm curl-tool:1.0
Output:
[+] Building 3.1s (6/6) FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load metadata for docker.io/library/alpine:3.20
=> [1/2] FROM docker.io/library/alpine:3.20
=> [2/2] RUN apk add --no-cache curl
=> exporting to image
curl 8.x.x
RUN executed while the image was being built. The final container does not run apk add; it starts from an image that already contains curl. Combining related package-manager work in one RUN instruction avoids extra layers and prevents temporary files from being saved in an earlier layer.
Example 2: COPY dependency files before source files
For a Node.js app, this layout keeps dependency installation cached when only server.js changes:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Build it like this:
docker build -t cache-friendly-api:1.0 .
Output on a later rebuild after editing only server.js may look like this:
[+] Building 1.0s (9/9) FINISHED
=> CACHED [2/5] WORKDIR /app
=> CACHED [3/5] COPY package*.json ./
=> CACHED [4/5] RUN npm ci --omit=dev
=> [5/5] COPY server.js ./
=> exporting to image
The package files did not change, so Docker reused the npm ci layer. This is one of the most important Dockerfile performance habits: copy slow-changing files first, run expensive setup, then copy fast-changing application code.
Example 3: ADD extracts a local tar archive
If the build context contains public.tar.gz, ADD can unpack it directly into the image:
FROM nginx:1.27-alpine
ADD public.tar.gz /usr/share/nginx/html/
EXPOSE 80
Build it:
docker build -t archived-site:1.0 .
Output:
[+] Building 2.2s (7/7) FINISHED
=> [internal] load build definition from Dockerfile
=> [internal] load metadata for docker.io/library/nginx:1.27-alpine
=> [1/2] FROM docker.io/library/nginx:1.27-alpine
=> [2/2] ADD public.tar.gz /usr/share/nginx/html/
=> exporting to image
This is a legitimate use of ADD: local tar extraction. If you wanted the archive file itself to appear in the image without extraction, use COPY public.tar.gz /some/path/ instead.
How It Works Step By Step
- The Docker client sends the Dockerfile and build context to the builder, excluding paths matched by
.dockerignore. - The builder resolves the base image named in
FROMand pulls any missing read-only layers. - For
COPY package*.json ./, Docker hashes the matching files and checks whether an identical copy step already exists in the cache. - For
RUN npm ci --omit=dev, Docker starts a temporary build container from the previous image state, runs the command, and records filesystem changes such asnode_modules. - For
COPY server.js ./, Docker stores the application file in a later layer. Changing this file invalidates this layer and following layers, but not the earlier dependency layer. - When a container is created from the final image, Docker mounts the image layers read-only and adds a thin writable container layer. Build-time
RUNcommands are already finished; onlyCMDorENTRYPOINTstarts at runtime.
Common Mistakes
Using ADD when COPY is enough
This works, but it hides intent:
FROM nginx:1.27-alpine
ADD index.html /usr/share/nginx/html/index.html
The file is just a local HTML file, so COPY is clearer:
FROM nginx:1.27-alpine
COPY index.html /usr/share/nginx/html/index.html
Invalidating the cache with COPY . . too early
This Dockerfile is common but slow for real projects:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["node", "server.js"]
Any changed source file can force npm ci to run again. Copy the package files first, install dependencies, then copy the source files. Also add a .dockerignore so node_modules, logs, and local build output do not enter the context.
Baking secrets into layers
Never do this with a real token:
FROM alpine:3.20
RUN echo "<YOUR_TOKEN>" > /tmp/token.txt
RUN rm /tmp/token.txt
Even though the later layer removes the file from the final visible filesystem, the earlier layer can still contain it. Secrets belong in runtime secret stores, mounted files, CI build secrets, or orchestrator-managed configuration, not in ordinary Dockerfile instructions.
Best Practices
- Prefer
COPYfor normal local files because it has fewer hidden behaviors. - Use
ADDonly for local tar extraction or when you deliberately need a remote source. - Order instructions from least-changing to most-changing so Docker can reuse expensive layers.
- Keep package-manager update, install, and cleanup work in the same
RUNinstruction when cleanup is needed. - Use pinned base image tags such as
node:20-alpineornginx:1.27-alpine. Avoidlatestfor reproducible builds. - Use
.dockerignoreto keep the build context small and to avoid copying private files by accident. - Use
COPY --chownandCOPY --chmodwhen ownership or permissions are known at copy time. - Do not store secrets with
RUN,COPY,ADD, orENV; image layers and history are not a secret store. - Remember that
EXPOSEis only image metadata. Publish ports withdocker run -por Composeports:.
Practice Exercises
- Write a Dockerfile for a small Node.js service that copies dependency manifests first, runs
npm ci --omit=dev, then copies onlyserver.js. The expected result is that editingserver.jsdoes not rerun dependency installation. - Create a Dockerfile that serves a prebuilt static site from
nginx:1.27-alpine. UseCOPYfor loose files, then tryADDwith a localpublic.tar.gzarchive and observe the difference. - Audit one of your own Dockerfiles. Find every
RUN,COPY, andADD, then decide whether each one is ordered well for caching and whetherADDis truly needed.
Summary
RUNexecutes commands at build time and saves filesystem changes into an image layer.COPYmoves files from the build context into the image and is the default choice for local files.ADDcan copy files, extract local tar archives, and fetch remote URLs, so use it deliberately.- Changing one layer invalidates that layer and every later layer, making instruction order critical.
- Copy dependency manifests before source files to preserve expensive dependency-install caches.
- Secrets copied or written during a build can remain in image layers even if a later instruction deletes them.
