Docker Best Practices Reference
This document summarizes official Docker best practices based on current recommendations from Docker documentation and industry standards.
Overview
Docker Best Practices Reference
This document summarizes official Docker best practices based on current recommendations from Docker documentation and industry standards.
General Principles
1. Create Ephemeral Containers
- Containers should be as stateless and ephemeral as possible
- Should be able to stop, destroy, and recreate with minimal setup
- Align with Twelve-Factor App methodology
2. Understand Build Context
- Use
.dockerignoreto exclude unnecessary files - Keep context size minimal for faster builds
- Don't include secrets or sensitive data in context
3. Use Multi-Stage Builds
- Separate build dependencies from runtime
- Dramatically reduce final image size
- Improve security by minimizing attack surface
4. One Concern Per Container
- Each container should address a single concern
- Makes containers more reusable and easier to scale
- Simplifies debugging and updates
Dockerfile Instructions Best Practices
FROM
Use specific tags, not :latest
# Bad
FROM node:latest
# Good
FROM node:21-alpine
# Better
FROM node:21-alpine@sha256:abc123...
Choose minimal base images
- Alpine Linux: ~5 MB base (vs ~80 MB for Ubuntu)
- Distroless: No shell, package manager (minimal attack surface)
- Scratch: Absolutely minimal (for static binaries)
Prefer official images
- Look for "Official Image" or "Verified Publisher" badges
- Official images are maintained and regularly updated
RUN
Chain commands to reduce layers
# Bad - creates 4 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN curl -sL https://example.com/script.sh | bash
# Good - creates 1 layer
RUN apt-get update && apt-get install -y --no-install-recommends \\
curl \\
vim \\
&& rm -rf /var/lib/apt/lists/* \\
&& curl -sL https://example.com/script.sh | bash
Clean up in same layer
# Package manager cache must be removed in same RUN
RUN apt-get update && apt-get install -y \\
package1 \\
package2 \\
&& rm -rf /var/lib/apt/lists/*
# For Alpine
RUN apk add --no-cache package1 package2
Use --no-install-recommends for apt
RUN apt-get install -y --no-install-recommends package
Pin package versions
# For apt
RUN apt-get install -y package=1.2.3-1
# For apk
RUN apk add package=1.2.3-r0
# For pip
RUN pip install package==1.2.3
Sort multi-line arguments
RUN apt-get update && apt-get install -y \\
curl \\
git \\
vim \\
wget \\
&& rm -rf /var/lib/apt/lists/*
Use pipefail for pipes
RUN set -o pipefail && wget -O - https://example.com | wc -l > /number
COPY vs ADD
Prefer COPY over ADD
# Use COPY for files and directories
COPY app.py /app/
# Only use ADD for auto-extraction or remote URLs
ADD https://example.com/file.tar.gz /tmp/
Use COPY --chown to avoid extra layer
# Bad - creates extra layer
COPY app.py /app/
RUN chown user:user /app/app.py
# Good - single layer
COPY --chown=user:user app.py /app/
WORKDIR
Use absolute paths
# Bad
WORKDIR app
# Good
WORKDIR /app
Don't use RUN cd
# Bad
RUN cd /app && npm install
# Good
WORKDIR /app
RUN npm install
USER
Don't run as root
# Create user
RUN groupadd -r appuser && useradd -r -g appuser appuser
# Or for Alpine
RUN addgroup -g 1001 -S appuser && adduser -S appuser -u 1001
# Switch to user
USER appuser
Use high UID (>10000) for better security
RUN useradd -u 10001 -m appuser
USER appuser
CMD and ENTRYPOINT
Use exec form for proper signal handling
# Bad - shell form (doesn't handle signals)
CMD python app.py
# Good - exec form
CMD ["python", "app.py"]
Combine ENTRYPOINT and CMD
# ENTRYPOINT defines the executable
ENTRYPOINT ["python"]
# CMD provides default arguments (can be overridden)
CMD ["app.py"]
EXPOSE
Document ports even though it doesn't publish
EXPOSE 8080
EXPOSE 443
HEALTHCHECK
Add health checks for services
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\
CMD curl -f http://localhost:8080/health || exit 1
LABEL
Add metadata
LABEL org.opencontainers.image.authors="team@example.com"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.description="Application description"
Build Optimization
Layer Caching
Order instructions from least to most frequently changing
# 1. Base image (rarely changes)
FROM node:21-alpine
# 2. System packages (rarely change)
RUN apk add --no-cache curl
# 3. Dependencies (change occasionally)
COPY package*.json ./
RUN npm ci
# 4. Source code (changes frequently)
COPY . .
Multi-Stage Builds
Separate build and runtime
# Build stage
FROM node:21 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime stage
FROM node:21-alpine
COPY --from=builder /app/dist /app
CMD ["node", "/app/index.js"]
BuildKit Features
Enable modern features
# syntax=docker/dockerfile:1
# Use cache mounts
RUN --mount=type=cache,target=/root/.cache/pip \\
pip install -r requirements.txt
# Use secret mounts (secrets not in final image)
RUN --mount=type=secret,id=aws,target=/root/.aws/credentials \\
aws s3 cp s3://bucket/file .
Security Best Practices
1. Scan Images
docker scan myimage:tag
# or
trivy image myimage:tag
2. Use Minimal Base Images
- Fewer packages = fewer vulnerabilities
- Alpine, distroless, or scratch
3. Don't Store Secrets in Images
# Bad
ENV DATABASE_PASSWORD=secret123
# Good - use runtime config or secrets
# Pass at runtime: docker run -e DATABASE_PASSWORD=...
4. Run as Non-Root
USER appuser
5. Use Read-Only Filesystem
docker run --read-only myimage
6. Limit Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage
Common Anti-Patterns
❌ Using :latest tag
- Unpredictable
- Not reproducible
- Can break without warning
❌ Not cleaning package cache
# Missing cleanup increases image by hundreds of MB
RUN apt-get update && apt-get install -y package
# Missing: && rm -rf /var/lib/apt/lists/*
❌ Running as root
- Security risk
- Violates principle of least privilege
❌ Installing unnecessary packages
# Bloated image
RUN apt-get install -y vim nano emacs curl wget
❌ Using ADD instead of COPY
- ADD has implicit behavior
- Can extract archives unexpectedly
❌ Multiple FROM in non-multi-stage context
- Creates confusion
- Use multi-stage builds properly