Dockerfile Best Practices
Comprehensive guide for creating optimized, secure, and fast Docker images using modern BuildKit features.
Workflow
- Identify language/framework → Pick template from Language Templates
- Apply essential rules → Every Dockerfile must follow Essential Rules
- Security hardening → Non-root user, pin versions, secrets management
- Optimize for cache → Separate deps from code, use cache mounts
- Multi-stage if needed → Compiled languages or distroless runtime
- Add metadata → OCI labels, HEALTHCHECK, STOPSIGNAL
- Review → Run
scripts/analyze_dockerfile.pyorscripts/analyze_compose.py
Essential Rules (Always Apply)
1. BuildKit syntax directive (first line, always)
# syntax=docker/dockerfile:1
2. Pin runtime versions, NOT OS versions
# ✅ GOOD
FROM python:3.12-slim
FROM node:22-alpine
FROM golang:1-alpine
# ❌ BAD - pins OS, blocks security updates
FROM python:3.12-slim-bookworm
FROM node:22-alpine3.19
3. Cache mounts for all package managers
# pip
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
# npm
RUN --mount=type=cache,target=/root/.npm npm ci
# yarn
RUN --mount=type=cache,target=/root/.yarn yarn install --frozen-lockfile
# go
RUN --mount=type=cache,target=/go/pkg/mod go mod download
# cargo
RUN --mount=type=cache,target=/usr/local/cargo/registry cargo build --release
# composer
RUN --mount=type=cache,target=/tmp/cache composer install --no-dev
# maven
RUN --mount=type=cache,target=/root/.m2 mvn package -DskipTests
4. APT cache setup (before any apt operation on Debian-based images)
RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends curl
5. Never use ARG/ENV for secrets
# ✅ GOOD - secret mount
RUN --mount=type=secret,id=api_key \
curl -H "Authorization: $(cat /run/secrets/api_key)" https://api.example.com
# ❌ BAD - exposed in image history
ARG API_KEY
6. Non-root user with UID/GID >10000
# Debian/Ubuntu
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app app
# Alpine
RUN addgroup -g 10001 app && adduser -u 10001 -G app -S app
7. Use COPY, never ADD
ADD has implicit behaviors (auto-extraction, URL downloads). Always use COPY.
8. Use COPY --chown instead of separate RUN chown
# ✅ GOOD - single layer, no extra overhead
COPY --chown=app:app . .
# ❌ BAD - doubles layer size
COPY . .
RUN chown -R app:app /app
9. OCI labels for metadata
LABEL org.opencontainers.image.source="https://github.com/org/repo" \
org.opencontainers.image.description="My application" \
org.opencontainers.image.version="1.0.0"
10. HEALTHCHECK for long-running services
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8000/health || exit 1
11. Create .dockerignore
Use template from assets/dockerignore-template. Critical for build context size and security.
Language Templates
Python (with uv - Recommended)
For detailed uv patterns, see references/uv_integration.md.
# syntax=docker/dockerfile:1
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app
# Install dependencies (cached layer)
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --locked --no-install-project
# Copy and install project
COPY --chown=app:app . .
RUN --mount=type=cache,target=/root/.cache/uv uv sync --locked
# Security
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app app
USER app
ENV PATH="/app/.venv/bin:$PATH"
CMD ["python", "-m", "myapp"]
Node.js
# syntax=docker/dockerfile:1
FROM node:22-alpine
WORKDIR /app
COPY package.json yarn.lock ./
RUN --mount=type=cache,target=/root/.yarn \
yarn install --frozen-lockfile --production
COPY --chown=app:app . .
RUN addgroup -g 10001 app && adduser -u 10001 -G app -S app
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "index.js"]
Go (Multi-stage)
# syntax=docker/dockerfile:1
FROM golang:1-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o main
FROM gcr.io/distroless/static
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]
Rust (Multi-stage)
# syntax=docker/dockerfile:1
FROM rust:1-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release
COPY . .
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
cargo build --release && cp target/release/myapp /usr/local/bin/
FROM gcr.io/distroless/cc
COPY --from=builder /usr/local/bin/myapp /myapp
ENTRYPOINT ["/myapp"]
PHP (with Composer)
# syntax=docker/dockerfile:1
FROM php:8-fpm-alpine
WORKDIR /app
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/cache \
composer install --no-dev --optimize-autoloader --no-scripts
COPY --chown=app:app . .
RUN composer dump-autoload --optimize
RUN addgroup -g 10001 app && adduser -u 10001 -G app -S app
USER app
EXPOSE 9000
CMD ["php-fpm"]
Debian-based (with APT cache)
# syntax=docker/dockerfile:1
FROM debian:stable-slim
RUN rm -f /etc/apt/apt.conf.d/docker-clean; \
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
WORKDIR /app
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
COPY --chown=app:app . .
RUN groupadd -r -g 10001 app && useradd -r -u 10001 -g app app
USER app
CMD ["./app"]
Docker Compose Rules
Critical rules (always enforce):
- Never use
version:— Deprecated since Compose V2 - Never use
container_name:— Prevents scaling and parallel environments - Never use
links:— Deprecated, use networks instead - Use specific image tags — Never
:latest - Use
depends_onwithcondition: service_healthy— Not baredepends_on
For complete Compose guide with examples, see references/compose_best_practices.md.
Build Commands Reference
# Basic build
docker buildx build -t myapp:1.0.0 .
# With remote cache (CI/CD)
docker buildx build \
--cache-from=type=registry,ref=registry.com/myapp:cache \
--cache-to=type=registry,ref=registry.com/myapp:cache,mode=max \
-t myapp:1.0.0 .
# With secrets
docker buildx build --secret id=api_key,src=./key.txt -t myapp:1.0.0 .
# Multi-platform
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:1.0.0 --push .
Analysis Tools
# Analyze Dockerfile
python scripts/analyze_dockerfile.py ./Dockerfile
# Analyze docker-compose.yml
python scripts/analyze_compose.py ./docker-compose.yml
Reference Documentation
For detailed information beyond what's covered here:
| Reference | Content |
|-----------|---------|
| references/optimization_guide.md | BuildKit internals, caching strategies, multi-stage patterns, distroless, profiling |
| references/best_practices.md | Complete checklist with impact levels, version pinning philosophy, UID/GID strategy |
| references/examples.md | Real-world before/after optimization examples (13+ scenarios) |
| references/uv_integration.md | Python with uv: installation methods, workspaces, multi-stage, all patterns |
| references/compose_best_practices.md | Complete Compose guide: networks, volumes, secrets, dev vs prod, scaling |