Skip to content

Dockerfile Best Practices for Production

Follow these best practices to build secure, fast, and slim Docker images for production.

1. Use Specific Base Image Tags

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

2. Multi-Stage Builds

Separate build dependencies from the runtime 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.

3. Optimize Layer Caching

Docker caches each layer. Order instructions from least to most frequently changing:

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

4. Run as Non-Root

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

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

5. Keep Images Small

# 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/*

6. Security Scanning

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

Also:

  • Use --no-cache-dir with pip
  • Don’t install build tools in production images
  • Scan images in CI/CD before pushing

7. Use .dockerignore

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

8. Set Correct Metadata

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

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

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

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