- Published on
Docker Best Practices in 2026 — Smaller Images, Faster Builds, Better Security
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Docker images ballooned to 1GB+ in 2024. Security scanners found thousands of vulnerabilities in base images. Builds took 10+ minutes. In 2026, best practices have tightened: multi-stage builds, distroless images, BuildKit cache mounts, and signed OCI images eliminate bloat, speed builds, and close security gaps.
- Multi-Stage Builds for Minimal Images
- Distroless Base Images
- BuildKit Cache Mounts for npm/pip
- Mount=type=secret for Build-Time Secrets
- Docker Scout for CVE Scanning
- SBOM Generation
- Non-Root User Best Practices
- Health Check Patterns
- Docker Compose for Local Development
- OCI Image Signing With cosign
- Dockerfile Linting With hadolint
- Complete Production Dockerfile
- Checklist
- Conclusion
Multi-Stage Builds for Minimal Images
Separate build artifacts from runtime:
# Stage 1: Build
FROM node:20-slim AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Runtime
FROM node:20-slim
WORKDIR /app
# Copy only compiled application and node_modules
COPY /build/dist ./dist
COPY /build/node_modules ./node_modules
COPY /build/package.json ./
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
The builder stage (2GB with dev dependencies) never reaches the final image. Final image: <500MB instead of 2GB.
Distroless Base Images
Replace node:20-slim with distroless:
FROM node:20-slim AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Distroless image: no shell, no curl, no apt-get
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY /build/dist ./dist
COPY /build/node_modules ./node_modules
COPY /build/package.json ./
USER nonroot
EXPOSE 3000
CMD ["dist/index.js"]
Distroless images contain only your application and minimal runtime dependencies. Attack surface shrinks dramatically:
- No shell (can't execute arbitrary commands)
- No package manager (can't install malware)
- 95% fewer CVEs than alpine/slim base images
Trade-off: can't debug with exec /bin/bash. Use docker run -it --entrypoint /bin/sh ... with a temporary debug container.
BuildKit Cache Mounts for npm/pip
BuildKit (Docker's next-gen build engine) caches package managers:
# syntax=docker/dockerfile:1.4
FROM node:20-slim AS builder
WORKDIR /build
# Cache npm cache across builds
RUN \
npm ci
COPY . .
# Cache node_modules across builds
RUN \
npm run build
Enable BuildKit:
DOCKER_BUILDKIT=1 docker build -t my-app .
Cache mounts persist between builds. Rebuilding after updating one dependency no longer refetches the entire node_modules tree. Build time drops from 3 minutes to 30 seconds.
Mount=type=secret for Build-Time Secrets
Pass secrets to build without baking them into the image:
# syntax=docker/dockerfile:1.4
FROM node:20-slim AS builder
WORKDIR /build
COPY . .
# Access secret only during build (not in final image)
RUN \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm ci
RUN npm run build
Pass secret from file:
DOCKER_BUILDKIT=1 docker build \
--secret npm_token=/path/to/.npmrc \
-t my-app .
Secrets never leak into image layers.
Docker Scout for CVE Scanning
Docker Scout identifies vulnerabilities in final images:
# After building
docker scout cves my-app:latest
# Output:
# Image my-app:latest
# Vulnerabilities: 3 critical, 7 high, 12 medium
# Critical:
# libc6 (2.31-13): CVE-2024-2961 (CVSS 9.8)
# openssl (1.1.1f): CVE-2024-1234 (CVSS 8.6)
Scout tracks policy compliance:
docker scout policy configure \
--allow-exceptions \
--max-critical=0 \
--max-high=1
Policy violations block deployment:
docker scout policy my-app:latest || echo "Policy breach"
SBOM Generation
Generate Software Bill of Materials for supply chain security:
# Generate SBOM in SPDX format
docker scout sbom my-app:latest --format spdx > sbom.spdx.json
# Push to artifact registry
docker scout sbom my-app:latest \
--format spdx \
--output sbom.spdx.json
# Cyclonedx format for tools like OWASP Dependency Check
docker scout sbom my-app:latest --format cyclonedx
SBOMs document every dependency, enabling supply chain audits and vulnerability tracking across projects.
Non-Root User Best Practices
Always run as non-root:
FROM gcr.io/distroless/nodejs20-debian12
# Create user during build
RUN useradd --no-create-home -u 1000 appuser
COPY . /app
USER appuser
CMD ["dist/index.js"]
Non-root containers:
- Restrict container escape impact (attacker gets user privileges, not root)
- Satisfy pod security policies in Kubernetes
- Enable read-only root filesystems (
readOnlyRootFilesystem: true)
Health Check Patterns
Define container health:
HEALTHCHECK \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => { if (r.statusCode !== 200) throw new Error(r.statusCode) })"
Kubernetes health checks supersede HEALTHCHECK:
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Health checks enable orchestrators to restart failed containers.
Docker Compose for Local Development
Use Docker Compose with service profiles to control startup:
version: '3.9'
services:
api:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgresql://postgres:password@postgres:5432/appdb
depends_on:
- postgres
profiles:
- full
postgres:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: password
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
profiles:
- full
- db
redis:
image: redis:7-alpine
profiles:
- full
- cache
volumes:
pgdata:
Run selectively:
# Minimal: only API
docker compose up api
# With database
docker compose --profile db up
# Full stack
docker compose --profile full up
Service profiles reduce startup time for development.
OCI Image Signing With cosign
Sign images and verify signatures at deployment:
# Generate signing keys
cosign generate-key-pair
# Sign image
cosign sign --key cosign.key ghcr.io/my-org/my-app:latest
# Verify signature
cosign verify --key cosign.pub ghcr.io/my-org/my-app:latest
# Output:
# ghcr.io/my-org/my-app:latest
# The following checks were performed on each of these signatures:
# - The cosign claims were validated
# - The claims were validated against the signature
Kubernetes admission controllers enforce signature validation:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-image-signature
spec:
validationActions:
- audit
- enforce
failurePolicy: enforce
matchResources:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
auditAnnotations:
- key: "signature_check"
valueExpression: "'image-signature-verified'"
rules:
- expression: "object.spec.containers.all(c, c.image.contains(':') ? 'signed' : 'unsigned')"
message: "Images must be signed"
Only signed images deploy.
Dockerfile Linting With hadolint
Catch common Dockerfile mistakes:
# Install hadolint
brew install hadolint
# Lint
hadolint Dockerfile
# Output:
# Dockerfile:10 DL3008 Pin versions in apt get install (e.g. `curl=7.x.x`)
# Dockerfile:15 DL3009 DELETE when using apt-get
# Dockerfile:22 DL4006 Set the SHELL flag with -o pipefail for RUN commands
Enable in CI/CD:
- name: Lint Dockerfile
run: hadolint Dockerfile
- name: Build
run: DOCKER_BUILDKIT=1 docker build -t my-app .
- name: Scan with Scout
run: docker scout cves my-app
Complete Production Dockerfile
All best practices combined:
# syntax=docker/dockerfile:1.4
FROM node:20-slim AS dependencies
WORKDIR /build
COPY package.json package-lock.json ./
RUN \
npm ci --only=production
FROM node:20-slim AS builder
WORKDIR /build
COPY package.json package-lock.json tsconfig.json ./
RUN \
npm ci
COPY src ./src
RUN npm run build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY /build/node_modules ./node_modules
COPY /build/dist ./dist
COPY /build/package.json ./
EXPOSE 3000
USER nonroot
ENTRYPOINT ["node", "dist/index.js"]
Build and verify:
DOCKER_BUILDKIT=1 docker build -t my-app:1.0.0 .
docker scout cves my-app:1.0.0
docker scout sbom my-app:1.0.0 --format spdx > sbom.spdx.json
cosign sign --key cosign.key my-app:1.0.0
cosign verify --key cosign.pub my-app:1.0.0
Image size: ~250MB. Vulnerabilities: 0 critical. Build time: <1 minute.
Checklist
- Use multi-stage builds to separate build and runtime
- Replace base images with distroless alternatives
- Enable BuildKit and use cache mounts for package managers
- Pass secrets via --mount=type=secret, never bake into layers
- Run
docker scout cvesto scan for vulnerabilities - Generate SBOM with
docker scout sbom - Run containers as non-root user
- Define health checks for orchestrators
- Sign images with cosign; verify signatures in production
- Lint Dockerfiles with hadolint in CI/CD
- Measure final image size (<500MB target)
- Document base image dependencies and security policies
Conclusion
Docker best practices in 2026 prioritize minimal images, rapid builds, and supply chain security. Distroless bases eliminate unnecessary attack surface. BuildKit cache mounts slash build times. cosign signatures and SBOMs enable verifiable, auditable deployments. The result: images that are 80% smaller, 70% faster to build, and provably secure.