Dockerfile Best Practices
Learn how to write efficient, secure, and maintainable Dockerfiles for production workloads.
Dockerfile Best Practices
Writing a Dockerfile is easy.
Writing a Dockerfile that is efficient, secure, and production-ready requires a deeper understanding of how Docker images are built.
This guide covers best practices that help:
- Reduce image size
- Improve build speed
- Increase security
- Simplify maintenance
- Optimize deployments
Why Dockerfile Quality Matters
A poorly written Dockerfile can lead to:
- Large image sizes
- Slow build times
- Security vulnerabilities
- Increased storage costs
- Slower deployments
Consider two images:
Poorly Optimized Image
├── Size: 1.5 GB
└── Build Time: 5 minutes
Optimized Image
├── Size: 150 MB
└── Build Time: 30 seconds
Small improvements can have a significant impact.
Use Specific Base Images
Avoid:
FROM node:latest
The latest tag changes over time and can introduce unexpected behavior.
Prefer:
FROM node:20
Or even:
FROM node:20-alpine
Benefits:
- Predictable builds
- Easier debugging
- Better reproducibility
Choose Lightweight Images
Many official images provide smaller variants.
Example:
FROM node:20-alpine
Compared to:
FROM node:20
Alpine-based images are significantly smaller and often ideal for production workloads.
Popular lightweight options:
- Alpine Linux
- Distroless Images
- Slim Variants
Leverage Layer Caching
Docker builds images layer by layer.
Example:
COPY . .
RUN npm install
This forces Docker to reinstall dependencies every build.
A better approach:
COPY package*.json ./
RUN npm install
COPY . .
Now dependencies are only reinstalled when package files change.
Benefits:
- Faster builds
- Improved CI/CD performance
- Reduced development time
Minimize Layers
Each Docker instruction creates a new layer.
Avoid:
RUN apt update
RUN apt install -y curl
RUN apt install -y git
Prefer:
RUN apt update && \
apt install -y curl git
This reduces image complexity and size.
Use .dockerignore
Docker sends files from the build directory to the daemon.
Some files should never be included.
Example:
node_modules
.git
.env
coverage
dist
Create:
.dockerignore
Example:
node_modules
.git
.env
coverage
dist
Benefits:
- Smaller build context
- Faster builds
- Improved security
Avoid Running as Root
By default, containers run as the root user.
Avoid:
FROM node:20
CMD ["node", "server.js"]
Instead:
FROM node:20
USER node
CMD ["node", "server.js"]
Benefits:
- Reduced attack surface
- Better security posture
- Compliance with container security standards
Use Multi-Stage Builds
Multi-stage builds separate build dependencies from runtime dependencies.
Example:
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
Benefits:
- Smaller images
- Cleaner runtime environments
- Reduced security risks
This is one of the most important Docker optimization techniques.
Keep Images Immutable
Containers should not modify application code after startup.
Avoid:
RUN git clone https://example.com/project.git
during runtime.
Instead:
- Build the application beforehand
- Package everything into the image
Images should be self-contained and reproducible.
Use COPY Instead of ADD
Docker provides two instructions:
COPY
ADD
Prefer:
COPY . .
Use ADD only when its advanced features are required.
For most use cases, COPY is simpler and more predictable.
Expose Only Required Ports
Avoid exposing unnecessary services.
Example:
EXPOSE 3000
Only expose ports that the application genuinely needs.
Use Environment Variables
Avoid hardcoding configuration values.
Instead:
ENV NODE_ENV=production
Or inject values during runtime:
docker run -e NODE_ENV=production my-app
This makes images more reusable.
Keep One Process Per Container
A container should generally run one primary process.
Good:
Container A → API
Container B → Database
Container C → Redis
Avoid:
Container
├── API
├── Database
├── Redis
└── Nginx
Smaller, focused containers are easier to maintain and scale.
Add Metadata
Labels provide useful metadata.
Example:
LABEL maintainer="team@example.com"
LABEL version="1.0"
LABEL description="Example API"
This can help with image management and automation.
Scan Images Regularly
Even official images may contain vulnerabilities.
Inspect images:
docker scout quickview
Or:
docker scan my-app
Regular security reviews should be part of your workflow.
Example Production Dockerfile
Node.js application:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
USER node
EXPOSE 3000
CMD ["npm", "start"]
This Dockerfile follows several best practices:
- Lightweight base image
- Layer caching
- Non-root user
- Minimal dependencies
- Predictable runtime behavior
Common Mistakes
Using latest Tags
Avoid:
FROM node:latest
Copying Everything
Avoid:
COPY . .
without a proper .dockerignore.
Running as Root
Avoid unnecessary privileges.
Large Images
Use lightweight base images whenever possible.
Storing Secrets
Never embed:
- Passwords
- API keys
- Tokens
inside Dockerfiles.
Use environment variables or secret management solutions instead.
Summary
A well-designed Dockerfile leads to faster builds, smaller images, and more secure deployments.
Key recommendations:
- Use specific image versions
- Choose lightweight base images
- Leverage build caching
- Create a
.dockerignore - Run containers as non-root users
- Use multi-stage builds
- Keep images immutable
- Avoid embedding secrets
Following these practices will help you create production-ready Docker images that are easier to maintain, deploy, and secure.