Mastering Docker Best Practices for 2026

May 5, 2026

Mastering Docker Best Practices for 2026

TL;DR

  • Use small, multi-stage builds to keep images lean and secure.
  • Always pin base image versions and scan for vulnerabilities regularly.
  • Treat containers as immutable and stateless — externalize configuration.
  • Employ CI/CD pipelines for automated builds, tests, and deployments.
  • Monitor, log, and secure containers continuously for production-grade reliability.

What You'll Learn

  • How to structure Dockerfiles for performance and security.
  • The difference between development and production containers.
  • How to use multi-stage builds and caching effectively.
  • Strategies for container monitoring, testing, and CI/CD integration.
  • Common pitfalls — and how to avoid them.

Prerequisites

To get the most from this guide, you should have:

  • Basic familiarity with Docker commands (docker build, docker run, docker compose).
  • Some experience with Linux command-line tools.
  • Optional but helpful: understanding of CI/CD systems like GitHub Actions or GitLab CI.

Introduction: Why Docker Best Practices Matter

Docker has revolutionized how we package and deploy applications. Containers encapsulate everything an app needs — dependencies, runtime, configuration — into a portable image that runs anywhere1. But with great power comes great responsibility: poorly designed Docker images can become bloated, insecure, and hard to maintain.

Following best practices isn’t just about elegance. It’s about performance, security, and scalability. In production systems, Docker best practices often translate directly into lower costs, faster deployments, and fewer outages.

This guide is current as of May 2026 — Docker Engine 29 is the latest stable release2, BuildKit has been the default builder since Engine 23.0 (Feb 2023), Compose v2 is the only supported Compose, and Docker Content Trust (DCT) is being retired in favour of Sigstore/Cosign and Notation.

Let’s dive in.


1. Building Efficient, Secure Docker Images

1.1 Use the Smallest Possible Base Image

Every extra layer in your Docker image increases attack surface and build time. Alpine-based images are popular because they’re tiny (usually under 10 MB) and still provide a full Linux environment3.

Base ImageSize (compressed, approx.)Use Case
ubuntu:24.04~29 MBGeneral-purpose, debugging-friendly
debian:bookworm-slim~28 MBStable, smaller than Ubuntu
alpine:3.23~3.5 MBMinimal, ideal for production builds
gcr.io/distroless/static~2 MBStatic Go/Rust binaries, no shell or package manager
cgr.dev/chainguard/static~1 MBContinuously rebuilt, near-zero CVEs4

A note on Alpine: musl libc and BusyBox-based tools can cause subtle compatibility issues for Python wheels and some glibc binaries. For Python and Node, python:3.13-slim or node:22-bookworm-slim are often safer defaults than Alpine.

Before:

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3
COPY . /app
CMD ["python3", "/app/main.py"]

After (optimized):

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "main.py"]

Benefits: Smaller image, faster pulls, fewer CVEs. (Using -slim over -alpine for Python avoids musl wheel-compatibility footguns.)

1.2 Use Multi-Stage Builds

Multi-stage builds let you separate build-time dependencies from runtime ones. This keeps your final image lean.

# Stage 1: Build
FROM golang:1.26 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app

# Stage 2: Runtime — distroless, no shell, no package manager
FROM gcr.io/distroless/static-debian12:nonroot
WORKDIR /app
COPY --from=builder /src/app .
USER nonroot:nonroot
ENTRYPOINT ["./app"]

This approach reduces image size dramatically — often by 80% or more — since you don’t carry compilers or headers into production. The distroless runtime stage drops the shell and apk/apt entirely, eliminating a large class of post-exploitation tooling.

1.3 Pin Image Versions

Avoid using latest tags. They change over time and can break builds unexpectedly. Note that Node.js 20 reached end of active LTS on October 13, 2024 and end of maintenance LTS on April 30, 2026 — for new projects in 2026, pin to the active 22 LTS line.

# Pin a specific minor in the current active LTS line (Node.js 22)
FROM node:22.14.0-bookworm-slim

Even better: pin by digest (FROM node@sha256:…) so the build is bit-for-bit reproducible even if a tag is republished. Pinning ensures reproducibility — crucial for CI/CD pipelines and security audits5.


2. Layer Caching and Build Performance

Docker caches layers to speed up builds. The order of instructions in your Dockerfile can make or break caching efficiency.

2.1 Order Matters

Put the most frequently changed lines at the bottom of your Dockerfile.

# Good
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

If you copy source files before installing dependencies, you’ll invalidate the cache every time you change your code.

2.2 Use .dockerignore

A .dockerignore file prevents unnecessary files (like .git, node_modules, or test data) from bloating your image.

Example:

.git
__pycache__
node_modules
tests
*.log

3. Security Best Practices

3.1 Run as a Non-Root User

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

This limits damage if your container is compromised6.

3.2 Scan for Vulnerabilities

Use tools like Trivy, Grype, or Docker Scout to detect CVEs in base images and dependencies.

trivy image myapp:latest

Output example:

Total: 5 (CRITICAL: 1, HIGH: 2, MEDIUM: 2, LOW: 0)

3.3 Keep Secrets Out of Images

Never bake API keys or credentials into images. At runtime, prefer environment variables, mounted files, or platform-native secrets:

docker run -e API_KEY=$API_KEY myapp:latest

At build time, use BuildKit's --mount=type=secret so secrets are never written into a layer:

# syntax=docker/dockerfile:1.7
RUN --mount=type=secret,id=npm_token \
    NPM_TOKEN=$(cat /run/secrets/npm_token) npm ci
DOCKER_BUILDKIT=1 docker build --secret id=npm_token,env=NPM_TOKEN -t myapp .

For sensitive deployments (e.g., Swarm or Kubernetes), use secret management systems like HashiCorp Vault or AWS Secrets Manager7.

3.4 Regularly Update Base Images

Outdated base images are a common source of vulnerabilities. Automate rebuilds weekly or monthly.


4. When to Use Docker vs When NOT to Use It

ScenarioUse DockerAvoid Docker
Microservices or APIs
Local development parity
GUI desktop apps
High-performance bare-metal workloads
CI/CD pipelines
Serverless functions⚙️ Sometimes⚙️ Sometimes

Docker shines in microservices, CI/CD, and cloud-native environments. But for GPU-heavy, real-time, or low-latency workloads, bare metal or specialized runtimes may be better.


5. Real-World Example: Docker at Scale

Large-scale services commonly rely on Docker for consistent deployments and rapid rollbacks8. For instance, companies often run containerized microservices behind orchestration platforms like Kubernetes or Amazon ECS.

A typical production workflow:

flowchart TD
    A[Developer Pushes Code] --> B[CI/CD Pipeline]
    B --> C[Docker Build & Scan]
    C --> D[Push to Registry]
    D --> E[Deploy to Kubernetes]
    E --> F[Monitoring & Alerts]

This pipeline ensures:

  • Automated builds and tests.
  • Security scanning before deployment.
  • Immutable, versioned images.

6. Testing and CI/CD Integration

6.1 Example: GitHub Actions CI Pipeline

Here’s a minimal CI pipeline that builds, scans, and pushes a Docker image.

name: CI
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v4
        with:
          username: ${{ secrets.DOCKER_USER }}
          password: ${{ secrets.DOCKER_TOKEN }}  # use a PAT, not your password

      - name: Build & push (BuildKit, multi-arch)
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          platforms: linux/amd64,linux/arm64
          tags: myorg/myapp:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Scan with Trivy (pinned, not @master)
        uses: aquasecurity/trivy-action@0.28.0
        with:
          image-ref: myorg/myapp:${{ github.sha }}
          exit-code: '1'
          severity: 'CRITICAL,HIGH'

A few things to call out about this pipeline:

  • We use the official docker/login-action instead of piping a password into docker login directly — it handles GHCR, ECR, and Docker Hub auth uniformly.
  • The Docker Hub credential is a personal access token, not the account password.
  • We pin aquasecurity/trivy-action to a tagged release rather than @master. (In March 2026, attackers compromised Aqua Security's CI and pushed malicious latest/recent tags of the Trivy Docker Hub image — pin everything in CI to a specific version, including actions.)
  • cache-from/cache-to with type=gha reuses BuildKit's layer cache across runs, often cutting build times by 60–80%.

This ensures every commit produces a verifiable, secure image.


7. Monitoring, Logging, and Observability

7.1 Logging

Redirect logs to stdout/stderr so they can be collected by Docker or orchestration systems.

import logging, sys
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
logging.info("App started")

Avoid writing logs to files inside containers — ephemeral storage will lose them.

7.2 Metrics and Health Checks

Expose health endpoints and metrics for observability. Pick a probe that exists in your base imagecurl is not installed in *-slim, distroless, or default Alpine images, so a curl-based HEALTHCHECK silently fails.

# Alpine and most -slim images: wget --spider works without extra tooling
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget --spider -q http://localhost:8080/health || exit 1

For distroless/scratch runtimes (no shell), ship a tiny health-check binary alongside your app and invoke it directly:

HEALTHCHECK --interval=30s --timeout=3s CMD ["/healthcheck"]

Integrate with monitoring tools like Prometheus, Datadog, or Grafana for visibility.


8. Common Pitfalls & Solutions

PitfallCauseSolution
Bloated imagesInstalling unnecessary packagesUse multi-stage builds and minimal bases
Inconsistent buildsUsing latest tagsPin versions
Build cache missesWrong Dockerfile orderInstall dependencies before copying code
Security leaksSecrets in imagesUse environment variables or secrets
Slow startupsHeavy init scriptsOptimize entrypoints

9. Troubleshooting Common Errors

Error: permission denied

Cause: Running as non-root without proper permissions.
Fix: Adjust file ownership or use USER directive correctly.

Error: no space left on device

Cause: Too many dangling images/containers.
Fix:

docker system prune -af

Error: connection refused

Cause: Service not exposed or wrong network mode.
Fix: Use EXPOSE and check docker network ls.


10. Try It Yourself: Build a Production-Ready Image

Step-by-Step

  1. Create a simple Flask app:

    from flask import Flask
    app = Flask(__name__)
    
    @app.route('/')
    def home():
        return 'Hello from Docker!'
    
    if __name__ == '__main__':
        app.run(host='0.0.0.0', port=8080)
    
  2. Create a Dockerfile:

    FROM python:3.13-slim
    WORKDIR /app
    
    # Create a non-root user up front so file ownership is correct
    RUN addgroup --system app && adduser --system --ingroup app app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY --chown=app:app . .
    USER app
    EXPOSE 8080
    HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
      CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://localhost:8080/').status==200 else 1)" || exit 1
    CMD ["python", "app.py"]
    
  3. Build and run:

    docker build -t flask-demo .
    docker run -p 8080:8080 flask-demo
    
  4. Visit http://localhost:8080 — you’re running a secure, production-grade container!


11. Common Mistakes Everyone Makes

  • Forgetting .dockerignore — leads to massive image sizes.
  • Using root — increases attack surface.
  • Mixing build and runtime dependencies — bloats images.
  • Ignoring health checks — no visibility into failing containers.
  • Skipping vulnerability scans — leaves you exposed.

12. Performance and Scalability Insights

  • Layer reuse: Smart layer ordering reduces build times by 60–80% in iterative development3.
  • Parallel builds: BuildKit has been the default builder since Docker Engine 23.0 (Feb 2023). You no longer need to set DOCKER_BUILDKIT=1. For concurrent multi-stage builds and remote cache (--cache-from type=registry,ref=...), use docker buildx.
  • Registry caching: Use local registries or mirrors to reduce network latency. In CI, prefer cache-from/cache-to with type=gha, type=registry, or type=s3.
  • Resource limits: Use --memory and --cpus flags to prevent noisy neighbors in production.

13. Security in Production

Follow the principle of least privilege. Combine Docker with:

  • AppArmor or SELinux profiles for isolation.
  • Read-only root filesystems (--read-only flag).
  • Network segmentation to isolate sensitive containers.

Note on Docker Content Trust (DCT): Docker has begun retiring DCT — the underlying Notary v1 codebase is no longer actively maintained, and the signing certificates for Docker Official Images began expiring in August 20259. Don't build new pipelines on DOCKER_CONTENT_TRUST=1. Instead, use Sigstore/Cosign (keyless signing with OIDC + transparency log) or Notation (Notary v2) to sign and verify images:

# Sign with Cosign (keyless, GitHub OIDC)
cosign sign myorg/myapp:${SHA}

# Verify before deploy
cosign verify myorg/myapp:${SHA} \
  --certificate-identity-regexp "https://github.com/myorg/.*" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com"

14. Testing Containers

Unit Testing

Use lightweight containers for isolated testing environments.

docker run --rm myapp pytest tests/

Integration Testing

Spin up dependent services using docker compose (the v2 plugin — docker-compose v1 reached end of life in June 2023). The top-level version: field is obsolete in Compose v2 and emits a warning if present, so omit it.

# No `version:` line — obsolete in Compose v2
services:
  db:
    image: postgres:17-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      retries: 10
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy

15. Production Readiness Checklist

✅ Use pinned, minimal base images.
✅ Run as non-root.
✅ Scan images regularly.
✅ Automate builds and deployments.
✅ Monitor logs and metrics.
✅ Use health checks and restart policies.
✅ Keep secrets external.


Conclusion

Docker is more than a packaging tool — it’s the backbone of modern software delivery. But to truly harness its power, you need discipline: small images, secure defaults, automated pipelines, and continuous monitoring.

These best practices aren’t theoretical; they’re what keep production systems stable at scale.


🧭 Key Takeaways

  • Smaller is safer: Use minimal base images and multi-stage builds.
  • Automate everything: CI/CD pipelines reduce human error.
  • Security first: Run as non-root, scan images, and manage secrets properly.
  • Monitor and test: Observability is essential for long-term stability.

Next Steps

  • Implement multi-stage builds in your current projects.
  • Add Trivy or Grype scanning to your CI pipeline.
  • Review your Dockerfiles for security and performance.
  • Subscribe to Docker’s official blog for updates.

Footnotes

  1. Docker Documentation – What is a Container? https://docs.docker.com/get-started/overview/

  2. Docker Engine 29 Release Notes https://docs.docker.com/engine/release-notes/29/

  3. Docker Official Images – Base Image Size Comparison https://hub.docker.com/_/alpine 2

  4. Chainguard – Minimal Container Images https://www.chainguard.dev/unchained/minimal-container-images-towards-a-more-secure-future

  5. Docker Docs – Best Practices for Writing Dockerfiles https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

  6. OWASP – Docker Security Cheat Sheet https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html

  7. HashiCorp Vault Documentation – Managing Secrets for Containers https://developer.hashicorp.com/vault/docs

  8. CNCF – Cloud Native Landscape Overview https://landscape.cncf.io/

  9. Docker – Retiring Docker Content Trust (2025) https://www.docker.com/blog/retiring-docker-content-trust/ 2

  10. Kubernetes Documentation – Container Runtimes https://kubernetes.io/docs/setup/production-environment/container-runtimes/

Frequently Asked Questions

Not always. Alpine’s musl libc can cause compatibility issues with some binaries — most notoriously, Python wheels (which target glibc) often have to compile from source on Alpine, making builds dramatically slower. For Python and Node, python:3.13-slim and node:22-bookworm-slim are typically safer choices than the Alpine variants. Use Alpine when you control the full stack — for example, statically linked Go binaries.

FREE WEEKLY NEWSLETTER

Stay on the Nerd Track

One email per week — courses, deep dives, tools, and AI experiments.

No spam. Unsubscribe anytime.