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 . .
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
COPYby default - it's explicit and predictable - Use
ADDonly for local archive auto-extraction --chown=user:groupsets ownership in the same layer; always use it when running as non-root- Copy dependency manifests before source code to maximise layer cache reuse
.dockerignoreis not optional - always create it before usingCOPY . .