Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

Dockerfile COPY: The Instruction Order That Decides Your Build Speed

Sean

Platform Writer

Jul 18, 2026
7 min read

COPY <src> <dest> copies files and directories from your build context into the image. The syntax is simple; the thing that actually matters is where you place it. Every COPY is a layer, and Docker caches layers - so if you copy your whole project before installing dependencies, any code change busts the dependency cache and reinstalls everything. Copy your dependency manifest first, install, then copy the source. That one ordering decision is the difference between a two-second rebuild and a two-minute one.

Dockerfile COPY: The Instruction Order That Decides Your Build Speed

COPY looks like the most boring instruction in a Dockerfile, and in isolation it is. Its impact is entirely about the layer cache, which is where slow builds and fast builds are actually decided.

Table of contents

The basic syntax

COPY source.txt /app/source.txt      # a file
COPY src/ /app/src/                   # a directory
COPY . /app                           # everything in the build context

COPY takes one or more sources from the build context - the directory you pass to docker build - and a destination inside the image. Trailing slashes matter: a destination ending in / is treated as a directory. Sources are relative to the build context root; you cannot copy a file from outside the context, which is a deliberate security boundary.

The build context itself is worth a thought. docker build . sends the entire current directory to the daemon before the build starts. If that directory holds a huge node_modules or .git, you are shipping all of it just to build. A .dockerignore file fixes that, and it is covered below.

Layer caching: the reason order matters

Each instruction in a Dockerfile creates a layer, and Docker reuses a cached layer as long as that instruction and everything before it are unchanged. The moment one layer changes, every layer after it is rebuilt.

Here is the slow version:

COPY . /app
RUN pip install -r requirements.txt

Change one line of source and COPY . /app produces a new layer, which invalidates the cache for the RUN pip install below it - so every code change reinstalls all dependencies. Painful.

Here is the fast version:

COPY requirements.txt /app/
RUN pip install -r requirements.txt
COPY . /app

Now the dependency manifest is copied and installed before the source. Change your code and only the final COPY . /app is rebuilt; the install layer is reused from cache because requirements.txt did not change. This is the single highest-value Dockerfile optimization, and it applies identically to package.json, go.mod, Gemfile, and every other manifest.

COPY versus ADD

Both copy files in; use COPY unless you specifically need what ADD does.

COPY app.tar.gz /app/       # copies the archive as-is
ADD app.tar.gz /app/        # copies AND auto-extracts the archive

ADD has two extra powers: it auto-extracts local tar archives, and it can fetch a URL. Both are more magic than you usually want. Auto-extraction surprises people who expected the file copied verbatim, and fetching a URL bypasses the layer cache and can pull a moving target.

The official guidance is unambiguous: prefer COPY for its predictability, and reach for ADD only when you genuinely want tar auto-extraction. For downloading a URL, an explicit RUN curl or wget is clearer and easier to reason about than ADD. When in doubt, COPY.

COPY in multi-stage builds

COPY --from is what makes multi-stage builds powerful - it pulls artifacts from an earlier stage without dragging that stage’s tooling into the final image:

FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN go build -o /app/server

FROM alpine:3.20
COPY --from=build /app/server /usr/local/bin/server
CMD ["server"]

The build stage has the whole Go toolchain; the final image has only the compiled binary. COPY --from=build reaches into the named stage and takes just the artifact you want. The result is a tiny production image with no compiler, no source, no build cache - only what runs.

This pattern is how you get a 15 MB image from a language that needs a 900 MB toolchain to build. If your images are large and full of build-time tools, multi-stage plus COPY --from is the fix.

COPY —chown and .dockerignore

Two practical details.

Set ownership as you copy, instead of a separate RUN chown:

COPY --chown=app:app . /app

That avoids an extra layer and gets the permissions right for a non-root runtime user in one step.

And keep junk out of the build context with a .dockerignore:

.git
node_modules
*.log
.env

.dockerignore works like .gitignore - listed paths are excluded from the context, so they are never sent to the daemon and never swept up by COPY . /app. This speeds up builds, shrinks images, and - crucially - keeps secrets like .env from being baked into a layer where anyone who pulls the image can read them. It is the first file to add to any project you containerize.

How this fits the rest of the stack

Fast, cache-friendly Docker builds are the difference between shipping a fix in seconds and waiting on a cold rebuild every time. The ordering discipline in a Dockerfile is exactly the kind of thing a good build pipeline should reward, so that the same image builds quickly whether it is on your laptop or in the platform that deploys it. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.

Useful related references:

FAQ

What does COPY do in a Dockerfile?

COPY <src> <dest> copies files and directories from your build context into the image. Each COPY creates a layer, and where you place it affects the build cache, so the ordering of your COPY instructions has a large impact on build speed.

Why should I copy requirements before source in a Dockerfile?

Because Docker caches layers. If you copy all your source before installing dependencies, any code change invalidates the install layer and reinstalls everything. Copy the manifest (requirements.txt, package.json) and install first, then copy the source - so code changes reuse the cached install.

What is the difference between COPY and ADD in a Dockerfile?

COPY copies files verbatim. ADD also auto-extracts local tar archives and can fetch URLs, which is more implicit behaviour than you usually want. Prefer COPY for predictability and use ADD only when you specifically need tar auto-extraction.

How does COPY —from work in multi-stage builds?

COPY --from=<stage> pulls an artifact from an earlier build stage into the current one, without bringing that stage’s tooling along. It lets you compile in a stage with the full toolchain, then copy only the resulting binary into a tiny final image.

What is a .dockerignore file for?

It excludes paths from the build context, like .gitignore for Docker. Listing .git, node_modules, logs, and .env keeps them out of the context so COPY . /app never sweeps them into the image - which speeds builds, shrinks images, and stops secrets being baked into a layer.

#dockerfile copy#docker#dockerfile#build cache#dev-infra