Dockerfile Explained
Understand Dockerfile instructions, image layers, build context, caching, multi-stage builds and best practices for creating efficient Docker images.
A Dockerfile is a text file that contains instructions for building a Docker image. Each instruction describes part of the environment that should be included in the image, such as the base operating system, application files, dependencies, environment variables and startup command.
Dockerfiles provide a repeatable way to package applications and their dependencies. Instead of manually configuring a server or development environment, developers can describe the required environment as code and use Docker to build the same image whenever it is needed.
What Is a Dockerfile?
A Dockerfile is usually named Dockerfile without a file extension. Docker reads the instructions inside the file from top to bottom and uses them to construct an image. The resulting image can then be used to create one or more containers.
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "start"]Dockerfile Build Process
When a Docker image is built, Docker processes the Dockerfile instructions and creates filesystem layers. These layers are combined into the final image. Some instructions create new layers, while others define metadata or configuration for how the resulting container should run.
- Docker reads the Dockerfile.
- The build context is sent to the Docker builder.
- Instructions are processed in order.
- Filesystem changes are stored in image layers.
- Docker produces the final image.
- A container can be created from the image.
Dockerfile vs Docker Image vs Container
| Concept | Purpose |
|---|---|
| Dockerfile | Instructions used to build an image |
| Docker image | Immutable package containing application files and dependencies |
| Docker container | Running or stopped instance created from an image |
Basic Dockerfile Structure
A Dockerfile commonly starts by selecting a base image, then defines a working directory, copies application files, installs dependencies, configures runtime settings and specifies the command that should start the application.
FROM base-image
WORKDIR /app
COPY package*.json ./
RUN install-dependencies
COPY . .
EXPOSE 3000
CMD ["start-command"]FROM Instruction
The FROM instruction defines the base image used to build the new image. It is one of the most important Dockerfile instructions because the selected base image determines the initial filesystem, available tools and runtime environment.
FROM node:22-alpineImages can be based on language runtimes such as Node.js, Python or Go, general-purpose Linux distributions or specialized images designed for particular workloads.
WORKDIR Instruction
WORKDIR sets the working directory for subsequent Dockerfile instructions such as RUN, COPY and CMD. If the directory does not exist, Docker creates it automatically.
WORKDIR /appCOPY Instruction
COPY transfers files and directories from the Docker build context into the image. It is commonly used to copy application source code, dependency manifests and configuration files.
COPY package*.json ./
COPY . .ADD Instruction
ADD can copy files into an image and provides additional behavior such as handling local tar archives. Although ADD has valid use cases, COPY is generally preferred when only straightforward file copying is required because its behavior is simpler and more explicit.
ADD application.tar.gz /app/RUN Instruction
RUN executes commands while the image is being built. It is commonly used to install packages, compile source code, generate files or prepare the application environment.
RUN npm ci
RUN npm run buildRUN commands execute during image construction rather than when a container starts. This distinction is important because dependencies installed by RUN become part of the resulting image.
CMD Instruction
CMD defines the default command that should run when a container is started from the image. Unlike RUN, CMD does not execute during the image build.
CMD ["npm", "start"]ENTRYPOINT Instruction
ENTRYPOINT defines the main executable for a container. It is useful when an image is intended to behave like a specific executable rather than simply providing a default command that can easily be replaced.
ENTRYPOINT ["node"]
CMD ["server.js"]| Instruction | Typical Role |
|---|---|
| RUN | Execute commands during image build |
| CMD | Default command when container starts |
| ENTRYPOINT | Define the main container executable |
CMD vs ENTRYPOINT
CMD and ENTRYPOINT are often confused because both influence how a container starts. CMD normally provides default arguments or a default command, while ENTRYPOINT establishes the executable that should be run.
FROM node:22-alpine
WORKDIR /app
COPY server.js .
ENTRYPOINT ["node"]
CMD ["server.js"]EXPOSE Instruction
EXPOSE documents the network ports that an application inside the image is expected to listen on. It does not publish the port to the host by itself.
EXPOSE 3000ENV Instruction
ENV defines environment variables inside the image. These variables can be available during later build steps and when containers created from the image run.
ENV NODE_ENV=production
ENV PORT=3000ARG Instruction
ARG defines build-time variables that can be supplied when an image is built. Unlike ENV, ARG is primarily intended for values needed during the build process.
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine| Instruction | Main Purpose |
|---|---|
| ARG | Build-time configuration |
| ENV | Environment configuration for the image and container |
USER Instruction
USER determines which user and group are used for subsequent Dockerfile instructions and by default when the container runs. Running applications as a non-root user can reduce the impact of certain security vulnerabilities.
USER nodeLABEL Instruction
LABEL adds metadata to an image. Labels can be used to describe the application, version, maintainer information or other attributes that can help automation and image management.
LABEL org.opencontainers.image.title="My Application"
LABEL org.opencontainers.image.version="1.0.0"SHELL Instruction
SHELL changes the default shell used by shell-form RUN instructions and related commands. It is particularly useful when an image requires a shell other than the default provided by the base image.
SHELL ["/bin/bash", "-c"]VOLUME Instruction
VOLUME declares a mount point intended for persistent or externally managed data. Volumes are useful when application data should not depend on the writable layer of a container.
VOLUME ["/var/lib/myapp/data"]Dockerfile Layers
Docker images are built from layers. Instructions that modify the filesystem can create layers that Docker can reuse during later builds. Understanding layers is important because unnecessary changes can increase image size and reduce build performance.
FROM node:22-alpine
↓
WORKDIR /app
↓
COPY package*.json ./
↓
RUN npm ci
↓
COPY . .
↓
CMD ["npm", "start"]Docker Build Cache
Docker can reuse previously built layers when the corresponding build step has not changed. This caching mechanism can make repeated builds significantly faster, especially when installing dependencies is expensive.
Ordering Instructions for Better Caching
A common optimization is to copy dependency manifests before copying the rest of the source code. If application source files change but package.json and the lockfile remain unchanged, Docker can often reuse the dependency installation layer.
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run buildBuild Context
The build context is the set of files available to the Docker builder during an image build. When a command such as docker build . is executed, the current directory is commonly used as the build context.
docker build -t my-app .The Dockerfile can only access files that are included in the build context. Sending unnecessary files also increases the amount of data that must be processed by the builder.
.dockerignore
A .dockerignore file excludes unnecessary files from the Docker build context. It commonly removes dependency directories, Git metadata, local environment files, build output and other files that should not be included in the build.
node_modules/
.git/
.env
dist/
coverage/
*.logMulti-Stage Builds
Multi-stage builds allow one Dockerfile to use multiple FROM instructions. Build tools and source dependencies can be kept in an intermediate stage while only the files required at runtime are copied into the final image.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/server.js"]The final stage contains only the runtime environment and selected build artifacts. This can significantly reduce the size of production images and avoid shipping compilers, source files or development tools that are unnecessary at runtime.
Development vs Production Dockerfiles
Development and production environments often have different requirements. Development images may include debugging tools, source maps and development dependencies, while production images should generally contain only what is required to run the application.
| Development | Production |
|---|---|
| Development dependencies | Runtime dependencies |
| Debugging tools | Minimal tooling |
| Source code | Built application |
| Hot reload | Stable startup command |
| Larger image | Optimized image |
Installing Dependencies
Dependencies should normally be installed during the image build rather than manually after a container starts. This makes the resulting image reproducible and ensures that containers created from the same image use the same dependency set.
COPY package*.json ./
RUN npm ciPinning Base Images
Using an explicit base image tag makes the intended runtime easier to understand and can improve reproducibility compared with relying on an unqualified or moving tag. For stricter reproducibility, image digests can also be used to identify an exact image version.
FROM node:22-alpineDockerfile Security
A Dockerfile is part of an application's supply chain, so its instructions should be treated as production code. The base image, packages installed during the build, copied files and runtime user can all affect the security of the resulting container.
- Use trusted and maintained base images.
- Keep base images and packages updated.
- Avoid embedding secrets in the image.
- Run applications as a non-root user when possible.
- Copy only required files into the image.
- Remove unnecessary build tools from production images.
- Scan images for known vulnerabilities.
Common Dockerfile Mistakes
Many Dockerfile problems come from treating the file like a simple installation script instead of an image definition. Poor instruction ordering, oversized build contexts and unnecessary packages can lead to slow builds, large images and avoidable security risks.
- Using unnecessarily large base images.
- Copying node_modules or other generated directories into the image.
- Forgetting to create a .dockerignore file.
- Installing development dependencies in production images.
- Embedding secrets directly in Dockerfile instructions.
- Running applications as root without a reason.
- Invalidating Docker cache unnecessarily.
- Leaving build tools inside the final runtime image.
Dockerfile Best Practices
- Choose a small and appropriate base image.
- Use WORKDIR instead of repeatedly changing directories.
- Copy dependency manifests before application source files.
- Use .dockerignore to keep the build context small.
- Prefer COPY for ordinary file copying.
- Use multi-stage builds for compiled or bundled applications.
- Keep production images limited to runtime requirements.
- Avoid storing secrets inside images.
- Run the application as a non-root user when practical.
- Keep Dockerfiles readable and organized.
Example Production Dockerfile
The following example demonstrates several common practices: dependency installation is separated from source copying, a production dependency set is used, the build occurs in a separate stage and the final image contains only the files required to run the application.
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]Building a Docker Image
Once a Dockerfile is ready, Docker can build an image from it using the docker build command. The -t option assigns a repository name and optional tag to the resulting image.
docker build -t my-app:1.0.0 .Running a Container from the Image
After building the image, a container can be created from it with docker run. Port publishing maps a host port to the port exposed by the application inside the container.
docker run --rm -p 3000:3000 my-app:1.0.0Dockerfile Instruction Reference
| Instruction | Purpose |
|---|---|
| FROM | Select the base image |
| WORKDIR | Set the working directory |
| COPY | Copy files from the build context |
| ADD | Copy files with additional archive-related behavior |
| RUN | Execute commands during the build |
| CMD | Set the default container command |
| ENTRYPOINT | Set the container's main executable |
| ENV | Define environment variables |
| ARG | Define build-time variables |
| EXPOSE | Document expected network ports |
| USER | Set the user for subsequent instructions and runtime |
| LABEL | Add image metadata |
| SHELL | Change the default shell |
| VOLUME | Declare a volume mount point |
Frequently Asked Questions
What is a Dockerfile used for?
A Dockerfile contains instructions that Docker uses to build an image. It defines the base image, application files, dependencies, configuration and default runtime behavior.
What is the difference between RUN and CMD?
RUN executes commands while the image is being built, while CMD defines the default command that runs when a container starts.
What is the difference between CMD and ENTRYPOINT?
ENTRYPOINT defines the main executable for a container, while CMD commonly provides a default command or default arguments that can be overridden depending on how the container is started.
Why should I use .dockerignore?
.dockerignore prevents unnecessary files from being sent as part of the Docker build context. This can make builds faster and reduce the chance of copying unwanted or sensitive files into an image.
What is a multi-stage Docker build?
A multi-stage build uses multiple FROM instructions so that build dependencies and tools can remain in an intermediate stage while only the required runtime files are copied into the final image.
Should Docker containers run as root?
Not necessarily. If the application does not require root privileges, running it as a non-root user can reduce the impact of certain security vulnerabilities.
Helpful Docker Tools
A Dockerfile Generator helps create Dockerfiles for common application stacks, a Docker Compose Generator creates multi-container Compose configurations, a Docker Ignore Generator builds .dockerignore files, an Environment Variable Generator helps prepare environment configuration, and a Docker Compose Formatter formats Compose files into a consistent readable structure.
Conclusion
A Dockerfile defines how a Docker image should be built and provides a reproducible way to package an application with its runtime environment. Understanding instructions such as FROM, COPY, RUN, CMD, ENTRYPOINT, ENV and USER makes it easier to create reliable images. By organizing instructions for effective caching, keeping the build context small, using multi-stage builds and avoiding unnecessary files and secrets, developers can build Docker images that are smaller, faster, more secure and easier to maintain.