Ctrl + K
Docker14 min read

Dockerfile Explained

Understand Dockerfile instructions, image layers, build context, caching, multi-stage builds and best practices for creating efficient Docker images.

Published: 2026-09-02

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

ConceptPurpose
DockerfileInstructions used to build an image
Docker imageImmutable package containing application files and dependencies
Docker containerRunning 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-alpine

Images can be based on language runtimes such as Node.js, Python or Go, general-purpose Linux distributions or specialized images designed for particular workloads.

💡 Prefer a small and appropriate base image when possible. Smaller base images generally reduce download size, build time and the number of packages that need to be maintained.

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 /app

COPY 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 . .
⚠️ Files outside the Docker build context cannot normally be copied into an image. Keep the build context focused and use .dockerignore to exclude unnecessary files.

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 build

RUN 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"]
InstructionTypical Role
RUNExecute commands during image build
CMDDefault command when container starts
ENTRYPOINTDefine 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"]
💡 Use the JSON array form of CMD and ENTRYPOINT when possible. It avoids an additional shell and provides more predictable signal handling for many applications.

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 3000
⚠️ EXPOSE does not make a container accessible from the host automatically. Ports are published when the container is started with an appropriate port mapping.

ENV 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=3000
⚠️ Do not put passwords, API keys or other secrets directly into ENV instructions in a Dockerfile. Values defined in an image can become part of the image configuration and may be exposed to people who can inspect the image.

ARG 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
InstructionMain Purpose
ARGBuild-time configuration
ENVEnvironment 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 node
💡 If the application does not require root privileges, prefer a non-root runtime user. Many official language images already provide an appropriate unprivileged user.

LABEL 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 build
⚠️ Copying the entire project before installing dependencies can invalidate the dependency layer whenever any source file changes, resulting in slower rebuilds.

Build 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/
*.log
💡 Use .dockerignore together with a Dockerfile. A smaller build context improves build performance and helps prevent unnecessary or sensitive files from being copied into the image.

Multi-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.

DevelopmentProduction
Development dependenciesRuntime dependencies
Debugging toolsMinimal tooling
Source codeBuilt application
Hot reloadStable startup command
Larger imageOptimized 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 ci

Pinning 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-alpine

Dockerfile 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.
⚠️ A secure Dockerfile cannot compensate for vulnerable dependencies or an outdated base image. Image security should be reviewed as part of the entire application supply chain.

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.
💡 A good Dockerfile should describe a reproducible application environment rather than a collection of manual setup steps. Keep the image focused, predictable and as small as practical.

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.0

Dockerfile Instruction Reference

InstructionPurpose
FROMSelect the base image
WORKDIRSet the working directory
COPYCopy files from the build context
ADDCopy files with additional archive-related behavior
RUNExecute commands during the build
CMDSet the default container command
ENTRYPOINTSet the container's main executable
ENVDefine environment variables
ARGDefine build-time variables
EXPOSEDocument expected network ports
USERSet the user for subsequent instructions and runtime
LABELAdd image metadata
SHELLChange the default shell
VOLUMEDeclare 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.

Found an issue?

Found an error, outdated information, or something missing from this article? Let me know through the Contact page.

Your feedback helps improve our articles and keep them accurate and useful.