Skip to content
Home
Dockerfile Best Practices for Production

Dockerfile Best Practices for Production

Docker Docker 8 min read 1659 words Beginner ExcellentWiki Editorial Team

Follow these best practices to build secure, fast, and slim Docker images for production. A well-written Dockerfile can reduce image size by 90%, improve build times through caching, and eliminate common security vulnerabilities. These patterns are gathered from real-world production deployments and Docker’s official recommendations.

1. Use Specific Base Image Tags

Always pin your base image to a specific version tag — or even better, a digest. Using :latest or omitting the tag leads to unpredictable builds that can break when a new version is released.

# Bad — unpredictable builds
FROM python

# Good — specific version
FROM python:3.11-slim

# Even better — pin to digest for reproducibility
FROM python:3.11-slim@sha256:abc123...

Pinning to a digest guarantees that every build uses exactly the same base image, even if the tag is updated upstream. This is critical for production deployments where reproducibility matters.

2. Multi-Stage Builds

Multi-stage builds are the single most impactful practice for reducing image size. Separate the build environment from the runtime environment so that build tools, compilers, and development dependencies never end up in the final image.

# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Runtime
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

The final image is ~25MB instead of ~300MB. The Node.js build tools, node_modules, source maps, and development files all stay in the builder stage and are discarded.

3. Optimize Layer Caching

Docker caches each layer in the image. When you rebuild, unchanged layers are reused from the cache. This means you should order instructions from least to most frequently changing to maximize cache hits.

# 1. Install system dependencies (rarely changes)
RUN apt-get update && apt-get install -y \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# 2. Copy dependency manifests (changes with each update)
COPY requirements.txt .

# 3. Install Python dependencies (cached if requirements.txt unchanged)
RUN pip install --no-cache-dir -r requirements.txt

# 4. Copy source code (changes most frequently)
COPY . .

In practice, this means dependency installation only runs when your dependency file changes. If you’re only editing application code, the COPY requirements.txt and RUN pip install layers are served from cache, making rebuilds nearly instant.

4. Run as Non-Root

By default, containers run as root. This is a security risk — if an attacker gains access to your container, they have root privileges. Always create a dedicated user.

# Good
RUN addgroup --system app && adduser --system --ingroup app app
USER app

# Even better — use the distroless image
FROM gcr.io/distroless/python3-debian11

Distroless images take this further by removing the shell, package manager, and any other tools that aren’t strictly needed at runtime. They contain only your application and its runtime dependencies, dramatically reducing the attack surface.

5. Keep Images Small

Smaller images mean faster pulls, lower storage costs, and fewer potential vulnerabilities. There are several strategies for minimizing image size:

# Use slim variants
FROM python:3.11-slim

# Or distroless (no shell, no package manager)
FROM gcr.io/distroless/base

# Clean up package manager cache in the same layer
RUN apt-get update && apt-get install -y \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

Always clean up package manager cache in the same RUN instruction where you install packages. If you clean up in a separate layer, the cache files are still present in the intermediate layer and contribute to the final image size.

6. Security Scanning

Modern Docker includes built-in vulnerability scanning through Docker Scout. Integrate scanning into your CI/CD pipeline to catch issues before deployment.

docker scout quickview my-image
docker scout recommendations my-image

Also:

  • Use --no-cache-dir with pip to avoid caching packages in the image
  • Don’t install build tools in production images
  • Scan images in CI/CD before pushing to your registry
  • Use trivy or grype for open-source scanning in CI pipelines

7. Use .dockerignore

A .dockerignore file prevents unnecessary files from being sent to the Docker daemon during the build. This speeds up builds and prevents secrets from leaking into images.

# .dockerignore
.git
__pycache__/
*.pyc
.env
node_modules/
dist/
*.md
tests/

8. Set Correct Metadata

Labels and health checks turn your image into a well-behaved production citizen. Labels help with organization and automation, while health checks let orchestrators know when your container is truly ready.

LABEL maintainer="team@example.com"
LABEL version="1.0.0"
LABEL description="Production API service"

# Health check
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

9. Use Build Arguments

Build arguments let you customize builds without changing the Dockerfile. Use them for environment-specific configurations, version numbers, or feature flags.

ARG NODE_ENV=production
ENV NODE_ENV=$NODE_ENV

ARG VERSION
LABEL version=$VERSION
docker build --build-arg VERSION=1.2.3 -t my-app .

10. Example: Production Flask App

Here’s a complete production Dockerfile that incorporates all the practices above:

FROM python:3.11-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM python:3.11-slim

RUN addgroup --system app && adduser --system --ingroup app app

WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

COPY app/ ./app/
COPY migrations/ ./migrations/

USER app

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=3s \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["gunicorn", "-w", "4", "-b", "0.0.0.0:8000", "app:create_app()"]

Size Comparison

ApproachImage Size
FROM python:latest~900MB
FROM python:3.11-slim~120MB
Multi-stage with distroless~50MB
Multi-stage + Alpine~30MB

FAQ

Why does my Docker build fail with “no matching manifest for linux/arm64”?

This happens when the base image doesn’t support your architecture. Use multi-architecture images or specify --platform linux/amd64 or --platform linux/arm64 in your FROM statement.

Should I use Alpine or slim as my base image?

Alpine is smaller (musl libc) but can cause issues with native Python/C extensions. Slim images (Debian-based) are more compatible. Start with slim; use Alpine only when size is critical.

How often should I rebuild my images?

Rebuild at least monthly to pick up security patches in base images. Use automated tools like Dependabot or Renovate to monitor base image updates and trigger rebuilds.

What is the difference between CMD and ENTRYPOINT?

CMD provides default arguments and can be overridden at runtime. ENTRYPOINT sets the main executable. Combined: ENTRYPOINT ["python"] with CMD ["app.py"] lets you run docker run myimage other.py to override the default script.

How do I debug a Docker build that fails silently?

Use DOCKER_BUILDKIT=1 docker build --progress=plain . for detailed output. Add RUN ls -la between build steps to inspect the filesystem. Use --no-cache to force a fresh build when debugging.


Related: Start with Docker for beginners and fix docker-compose errors.

Build Performance Optimization

Layer caching is the most impactful optimization for Docker builds. Order Dockerfile commands from least to most frequently changing: base image, system dependencies, application dependencies, source code, build step, runtime configuration. Use BuildKit (# syntax=docker/dockerfile:1) for concurrent builds and better caching. BuildKit’s --mount=type=cache preserves package caches across builds. In CI, pull the previous image as a cache source. Minimize build context size with .dockerignore — include only files needed for the build. For monorepos, use Docker’s context and dockerfile options to build efficiently.

CI/CD Pipeline Integration

Dockerfile best practices extend naturally into CI/CD pipelines. Use docker build --cache-from to seed the build cache from a previously pushed image. Tag images with both the commit SHA and semantic version for traceability: myapp:1.2.3 and myapp:abc123def. Run security scans as a build step: trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:latest. Sign images with Cosign for software supply chain attestation. Store Docker Hub or registry credentials in CI secret variables rather than in Dockerfiles. For multi-architecture deployments, use docker buildx build --platform linux/amd64,linux/arm64 --push to create a manifest list. Configure automated rebuilds with Dependabot or Renovate to keep base images patched against known vulnerabilities.

Language-Specific Dockerfile Patterns

Node.js: Use node:20-alpine, copy package*.json first, run npm ci --only=production. Python: Use python:3.12-slim-bookworm, use --no-cache-dir with pip. Go: Multi-stage from golang:1.22-alpine builder to scratch runtime. Java: Maven builder to JRE runtime. Rust: Compile with --release in builder, copy to slim runtime. Always create a non-root user and switch to it before running the application. For Ruby, use ruby:3.3-slim-bookworm and bundle install --without development --deployment. For PHP, use php:8.3-fpm-alpine with docker-php-ext-install for extensions. For .NET, use mcr.microsoft.com/dotnet/sdk:8.0 for builder and mcr.microsoft.com/dotnet/aspnet:8.0 for runtime, publishing with dotnet publish -c Release -o /app. Regardless of language, always pin patch versions in production Dockerfiles and test base image updates in CI before deploying. Following these patterns consistently across all services in your organization ensures that every team produces images that are fast to build, secure to deploy, and easy to debug in production.

Related Concepts and Further Reading

Understanding dockerfile best practices requires familiarity with several interconnected ideas and principles that together form a complete picture. Exploring these related concepts deepens your knowledge and provides context that makes the core material more meaningful and applicable. Each concept builds on the others, creating a web of understanding that supports deeper learning and practical application. Taking time to explore how these elements connect reveals patterns that accelerate comprehension and retention of new information.

The relationship between dockerfile best practices and adjacent fields is worth particular attention. Many of the most important insights emerge at the boundaries between disciplines, where ideas from different areas combine to create new approaches and solutions that neither field could produce alone. Exploring these connections pays dividends in both breadth and depth of understanding, revealing patterns and principles that might otherwise remain hidden from view. Cross-disciplinary knowledge is increasingly valued as problems become more complex and interconnected.

For those looking to go beyond introductory material, several excellent resources provide deeper treatment of specific aspects of dockerfile best practices. Academic journals, industry publications, authoritative reference works, and online courses each offer different perspectives and levels of detail. The key is to match your reading to your current learning goals and build knowledge progressively, focusing on quality over quantity in your study materials. A well-chosen resource that matches your current level is worth more than dozens of resources that are too basic or too advanced.

Section: Docker 1659 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top