COPY & ADD

Copy files into your image correctly. Learn the difference between COPY and ADD, how to set ownership, use .dockerignore, and avoid common mistakes.

COPY & ADD

COPY and ADD both bring files from your host (the build context) into the image. COPY does one thing simply and explicitly. ADD does more - sometimes too much. Knowing when to use each prevents surprises.

COPY - The Default Choice

COPY copies files and directories from the build context into the image filesystem. What you see is what you get.

# Single file
COPY server.js /app/server.js

# Into a directory (trailing slash = directory target)
COPY server.js /app/

# Multiple sources into a directory
COPY package.json package-lock.json /app/

# Wildcard - all .json files
COPY *.json /app/

# Entire directory
COPY ./src /app/src

# Current directory into WORKDIR
COPY . .

Source paths are relative to the build context (the . in docker build -t myapp .). Destination paths are absolute inside the image, or relative to WORKDIR.

COPY with --chown

By default, copied files are owned by root. Use --chown to set ownership in the same layer - essential when running as a non-root user:

FROM node:20-alpine
WORKDIR /app

# Files owned by the node user (built into the node image)
COPY --chown=node:node package*.json ./
RUN npm ci
COPY --chown=node:node . .

USER node
CMD ["node", "server.js"]

Without --chown, the node user can't read or write files owned by root - a common cause of permission errors.

You can also use numeric UID/GID (more portable across images):

COPY --chown=1000:1000 . .

COPY with --chmod

Set file permissions directly:

# Make a script executable
COPY --chmod=755 entrypoint.sh /entrypoint.sh

# Read-only config
COPY --chmod=644 config.yaml /app/config.yaml

Multi-Stage COPY: --from

Copy files from a previous build stage or external image instead of the host:

FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
# Copy only the compiled output from the builder stage
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules

Copying Dependency Files First (Cache Pattern)

The most important COPY pattern for build speed - copy only the dependency manifest, install, then copy source code:

#  Optimized - npm ci only reruns when package.json changes
COPY package*.json ./
RUN npm ci
COPY . .

# Unoptimized - npm ci reruns every time any file changes
COPY . .
RUN npm ci

Apply the same pattern in every language:

# Python
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# Go
COPY go.mod go.sum ./
RUN go mod download
COPY . .

# Ruby
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
Exercise

COPY with Ownership

You are writing a Dockerfile based on node:20-alpine with WORKDIR /app. You want to copy package.json into the image owned by the "node" user, install dependencies, and then copy the rest of the source - also owned by "node". Which snippet is correct?

ADD - When to Use It

ADD has two capabilities beyond COPY:

1. Auto-extract local archives:

# .tar.gz, .tar.bz2, .tar.xz are auto-extracted
ADD app.tar.gz /app/
# Equivalent to: COPY app.tar.gz /tmp/ && tar xzf /tmp/app.tar.gz -C /app/

2. Download from URLs:

ADD https://example.com/config.json /app/config.json

However, the URL form has problems: it runs at build time (not cached well), can't verify the file's integrity, and creates a single layer with the file already present (can't clean up in the same layer). For URLs, prefer RUN curl or RUN wget instead:

#  Better than ADD for URLs - you can verify and clean up
RUN curl -fsSL https://example.com/install.sh | sh

#  Or download, verify, install, clean up in one layer
RUN curl -fsSL -o /tmp/tool.tar.gz https://example.com/tool.tar.gz && \
    echo "abc123  /tmp/tool.tar.gz" | sha256sum -c && \
    tar xzf /tmp/tool.tar.gz -C /usr/local/bin && \
    rm /tmp/tool.tar.gz

Rule of thumb: Use COPY by default. Only use ADD when you specifically need local archive auto-extraction.

The .dockerignore File

COPY . . copies everything in the build context unless you tell Docker otherwise. A .dockerignore file works exactly like .gitignore:

# .dockerignore

# Dependencies (rebuilt inside image anyway)
node_modules/
vendor/
__pycache__/
*.pyc
.venv/

# Version control
.git/
.gitignore

# Secrets and local config
.env
.env.*
*.pem
*.key

# Build artifacts
dist/
build/
*.egg-info/
target/

# IDE and OS files
.vscode/
.idea/
.DS_Store
Thumbs.db

# Test and docs (not needed at runtime)
tests/
test/
spec/
docs/
*.md
coverage/

Why it matters:

  • Prevents secrets from leaking into images
  • Dramatically reduces build context size (speeds up docker build)
  • Avoids cache invalidation from irrelevant file changes

Always create .dockerignore before writing COPY . ..

Common Mistakes

Copying too early:

# COPY . . before installing deps busts the cache every build
COPY . .
RUN npm install

#  Copy deps manifest first
COPY package*.json ./
RUN npm install
COPY . .

Forgetting .dockerignore:

# This copies node_modules (hundreds of MB) if .dockerignore is missing
COPY . .

Wrong destination syntax:

# This creates a FILE called /app (not a directory!)
COPY server.js /app

#  Trailing slash = directory
COPY server.js /app/

#  Or set WORKDIR first
WORKDIR /app
COPY server.js .

Summary

  • Use COPY by default - it's explicit and predictable
  • Use ADD only for local archive auto-extraction
  • --chown=user:group sets ownership in the same layer; always use it when running as non-root
  • Copy dependency manifests before source code to maximise layer cache reuse
  • .dockerignore is not optional - always create it before using COPY . .

Continue Learning

Explore Related Topics

Try the Tool

Related Resources