20.1.2.3 First App File Copy
A focused guide to First App File Copy, connecting core concepts with practical Docker and container operations.
Copying application files into a Docker image is done with the COPY instruction in a Dockerfile. Understanding how COPY works — where it reads from, where it writes to, how paths are resolved, and how it interacts with layer caching — is essential for building images that contain exactly the right files.
The Build Context
When docker build runs, Docker packages a directory — the build context — and sends it to the build daemon. The build context is specified as the final argument to docker build:
docker build -t my-app .
The . means the current directory is the build context. Every COPY instruction in the Dockerfile reads source files from this context directory. Files outside the build context cannot be copied into the image. Files explicitly excluded by .dockerignore are not available to COPY even if they are physically inside the context directory.
Basic COPY Syntax
COPY <source> <destination>
<source>is a path relative to the build context root.<destination>is an absolute path inside the image, or a path relative to theWORKDIR.
Copy a single file:
COPY server.js /app/server.js
Copy with a destination that is the current WORKDIR:
WORKDIR /app
COPY server.js .
The . destination means "the current WORKDIR", so the file lands at /app/server.js.
Copy multiple files by listing them:
COPY package.json package-lock.json ./
The trailing ./ is the destination directory. Both files are placed in the WORKDIR.
Copy an entire directory:
COPY src/ ./src/
All files inside src/ on the host are copied into /app/src/ in the image.
Copy everything in the build context:
COPY . .
This copies all files not excluded by .dockerignore from the build context root into the WORKDIR.
How COPY Creates a Layer
Each COPY instruction creates a new image layer. The layer contains only the files that were added or changed by that instruction. This matters for caching: if the files specified in a COPY instruction have not changed since the last build, Docker reuses the cached layer and skips executing the instruction again.
This is why dependency manifest files are typically copied before application source code:
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
If only source files change (not package.json), Docker reuses the cached RUN npm ci layer and only re-executes the final COPY . .. If COPY . . appeared before RUN npm ci, any file change would force a full reinstall.
COPY with Wildcards
COPY supports shell-style glob patterns:
COPY *.json ./
Copies all .json files from the build context root into the WORKDIR.
COPY config/*.yaml /app/config/
Copies all .yaml files from the config/ directory in the build context into /app/config/ in the image.
Preserving Directory Structure
When the source is a directory, the contents of the directory are copied (not the directory itself):
COPY src/ ./src/
If src/ contains index.js and utils/helper.js, the image will contain /app/src/index.js and /app/src/utils/helper.js.
If you write:
COPY src/ ./
The contents of src/ are placed directly in the WORKDIR, without the src/ subdirectory.
File Ownership and Permissions
By default, files copied into an image are owned by root:root. If the container runs as a non-root user, the files may not be writable. Use the --chown flag to set ownership at copy time:
COPY --chown=node:node . .
This is especially relevant for Node.js applications, where the official images include a node user intended for running the application without root privileges:
FROM node:20-alpine
WORKDIR /app
COPY --chown=node:node package.json package-lock.json ./
RUN npm ci
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
The ADD Instruction
ADD is a broader version of COPY. It does everything COPY does and also:
- Fetches files from URLs (not recommended — it bypasses caching and prevents verification).
- Automatically extracts tar archives:
ADD archive.tar.gz /app/extracts the contents into/app/.
For copying local files, use COPY. Its behavior is predictable and explicit. Use ADD only when you specifically need tar extraction behavior.
What to Include in the Build Context
The build context is everything that can be copied into the image. It should be as small as possible because Docker sends the entire context directory to the build daemon at the start of every build, even files that COPY never touches.
Create a .dockerignore file in the same directory as the Dockerfile to exclude files:
node_modules
.git
*.log
.env
dist
coverage
Without .dockerignore, COPY . . includes node_modules (which can be hundreds of megabytes), .git history, environment files with secrets, and test output directories. These inflate the image, slow builds, and can expose sensitive data.
Verifying What Was Copied
After building, run an interactive shell in the image to verify the file layout:
docker run --rm -it my-app sh
Inside the container:
ls -la /app
find /app -type f
On Alpine-based images, use sh. On Debian/Ubuntu-based images, use bash.
Alternatively, inspect the image's filesystem layer by layer:
docker history my-app
This shows each layer and the size it contributed, which reveals whether COPY instructions are adding the expected amount of data.
A Complete Example
Project structure on the host:
my-project/
Dockerfile
.dockerignore
package.json
package-lock.json
src/
index.js
routes/
api.js
.dockerignore:
node_modules
.git
*.log
Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY src/ ./src/
EXPOSE 3000
CMD ["node", "src/index.js"]
Build:
docker build -t my-project .
The image contains /app/package.json, /app/package-lock.json, the installed node_modules, and the src/ directory tree — nothing else from the project directory.