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-debian115. 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-imageAlso:
- Use
--no-cache-dirwith 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 19. Use Build Arguments
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
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 |
Related: Start with Docker for beginners and fix docker-compose errors.