Dockerfile Best Practices for Production
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-debian11Distroless 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-imageAlso:
- Use
--no-cache-dirwith 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
trivyorgrypefor 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 19. 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=$VERSIONdocker 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
| Approach | Image 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 Articles
Docker
Docker Image Optimization: 90% Size Reduction Guide
Optimize Docker images with multi-stage builds, slim base images, layer caching, .dockerignore, security scanning, and production-ready Dockerfile patterns.
DevOps
Docker Compose for Production: Tips and Best Practices
Learn Docker Compose best practices for production. Covers multi-stage builds, health checks, logging, secrets, and orchestration.
Docker
Docker Security: Best Practices for Container Security
Learn Docker security — image scanning, least privilege, secrets management, running containers securely, and compliance.
API Development
API Security Best Practices for Production Systems
Learn API security best practices — authentication, authorization, input validation, rate limiting, CORS, and protecting sensitive data.
API Development
API Versioning: Strategies for Backward Compatibility
Learn API versioning strategies — URI versioning, header versioning, query parameters, and maintaining backward compatibility for production APIs.