Writing a Dockerfile is easy. Writing a good Dockerfile that produces optimized, secure, and maintainable images requires knowledge of best practices and common pitfalls. In this guide, we cover the essential techniques for building production-ready container images in 2026.
Start with the Right Base Image
Your base image choice has a massive impact on image size and security:
# Bad: Full Ubuntu image (~77MB)
FROM ubuntu:22.04
# Better: Slim variant (~25MB)
FROM python:3.12-slim
# Best for compiled languages: Distroless (~2MB)
FROM gcr.io/distroless/static-debian12
Alpine-based images are small but may cause compatibility issues with some Python packages due to musl libc. Test thoroughly before using Alpine in production.
Multi-Stage Builds
Multi-stage builds are the most powerful optimization technique. They allow you to use build tools in one stage and copy only the compiled output to the final image:
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /server
# Stage 2: Production
FROM gcr.io/distroless/static-debian12
COPY --from=builder /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
Optimize Layer Caching
Docker caches each layer. Order your instructions from least to most frequently changing:
# Good: Dependencies cached separately from code
FROM node:20-slim
WORKDIR /app
# These change rarely - cached
COPY package.json package-lock.json ./
RUN npm ci --production
# This changes frequently - not cached
COPY . .
RUN npm run build
Minimize Layer Count
Combine related commands into single RUN instructions:
# Bad: Multiple layers
RUN apt-get update
RUN apt-get install -y curl wget
RUN apt-get clean
# Good: Single layer with cleanup
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl wget && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Security Best Practices
Running containers as root is a security risk. Always create and use a non-root user:
FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
RUN pip install --no-cache-dir -r requirements.txt
USER appuser
CMD ["python", "app.py"]
Use .dockerignore
Always include a .dockerignore file to exclude unnecessary files from the build context:
.git
.env
node_modules
__pycache__
*.md
docker-compose*.yml
.github
Health Checks
Add HEALTHCHECK instructions so Docker can monitor container health:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Environment Variables and Secrets
Never hardcode secrets in Dockerfiles. Use build arguments for non-sensitive configuration and runtime environment variables for secrets:
ARG APP_VERSION=latest
ENV APP_VERSION=${APP_VERSION}
# Never do this:
# ENV DATABASE_PASSWORD=mysecret
Image Scanning
Regularly scan your images for vulnerabilities:
docker scout cves myimage:latest
trivy image myimage:latest
Checklist for Production Dockerfiles
- Use specific image tags, never
latest - Implement multi-stage builds
- Run as non-root user
- Include health checks
- Minimize installed packages
- Use
.dockerignore - Scan for vulnerabilities regularly
- Label images with metadata
Following these best practices will result in container images that are smaller, faster to build, more secure, and easier to maintain. Start applying them to your projects today and see the difference in your deployment pipeline.