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 | bashClean 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 package2Use --no-install-recommends for apt
RUN apt-get install -y --no-install-recommends packagePin 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.3Sort 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 > /numberCOPY 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 /appDon't use RUN cd
# Bad
RUN cd /app && npm install
# Good
WORKDIR /app
RUN npm installUSER
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 appuserUse high UID (>10000) for better security
RUN useradd -u 10001 -m appuser
USER appuserCMD 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 443HEALTHCHECK
Add health checks for services
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1LABEL
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:tag2. 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 appuser5. Use Read-Only Filesystem
docker run --read-only myimage6. Limit Capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimageCommon 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