Container Image Optimisation
Essential techniques and patterns for building efficient, secure, and maintainable container images.
Container Image Optimisation
Essential techniques and patterns for building efficient, secure, and maintainable container images.
Overview
Container image optimisation focuses on reducing image size, improving build performance, enhancing security, and maintaining supply chain integrity. Optimised images deploy faster, consume fewer resources, reduce attack surface, and lower storage and transfer costs across your infrastructure.
flowchart TB
subgraph "Image Optimisation Pipeline"
A[Source Code] --> B[Multi-stage Build]
B --> C[Layer Optimisation]
C --> D[BuildKit Cache]
D --> E[Security Scanning]
E --> F[Image Signing]
F --> G[SBOM Generation]
G --> H[Optimised Image]
style B fill:#e1f5ff
style D fill:#e1f5ff
style E fill:#ffe1e1
style F fill:#ffe1e1
style G fill:#ffe1e1
end
Multi-stage Builds
Multi-stage builds separate build-time dependencies from runtime requirements, dramatically reducing final image size and eliminating unnecessary tooling.
Key Concepts
Multi-stage Dockerfiles use multiple FROM statements, each starting a new stage. Artifacts are selectively copied between stages, leaving behind compilers, build tools, and intermediate files. This approach is essential for compiled languages and complex build processes.
flowchart LR
subgraph "Stage 1: Build"
A[Base Image<br/>golang:1.21] --> B[Install Dependencies]
B --> C[Compile Binary]
end
subgraph "Stage 2: Runtime"
D[Minimal Base<br/>alpine:3.19] --> E[Copy Binary]
E --> F[Final Image<br/>~20MB]
end
C -.->|COPY --from| E
style A fill:#fff4e1
style D fill:#e1ffe1
style F fill:#e1ffe1
Basic Multi-stage Pattern
# Stage 1: Build environment
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o app ./cmd/app
# Stage 2: Runtime environment
FROM alpine:3.19
# Add certificates for HTTPS
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /build/app .
USER nobody:nobody
ENTRYPOINT ["/app/app"]
Node.js Multi-stage Build
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Stage 2: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 3: Production
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Copy only production dependencies
COPY --from=deps /app/node_modules ./node_modules
# Copy built application
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER node
CMD ["node", "dist/main.js"]
Python Multi-stage with Virtual Environment
# Stage 1: Build dependencies
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
# Create virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim
WORKDIR /app
# Copy virtual environment from builder
COPY --from=builder /opt/venv /opt/venv
# Set PATH to use virtual environment
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
COPY . .
USER nobody
CMD ["python", "app.py"]
Advanced: Named Stages for Testing
# Base stage with common dependencies
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
# Development stage
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]
# Test stage
FROM development AS test
RUN npm run test
RUN npm run lint
# Build stage
FROM base AS build
RUN npm ci --only=production
COPY . .
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./
USER node
CMD ["node", "dist/main.js"]
Build Specific Stages
# Build development image
docker build --target development -t myapp:dev .
# Build and run tests
docker build --target test -t myapp:test .
# Build production image (default final stage)
docker build -t myapp:prod .
# Build specific stage with custom cache
docker build --target production \
--cache-from myapp:cache \
-t myapp:latest .
Cache Strategies
Effective caching reduces build times by reusing layers that haven't changed. Layer ordering and cache mounts are crucial for optimal performance.
Key Concepts
Docker builds images layer by layer. Each instruction creates a new layer, and Docker caches these layers. If a layer changes, all subsequent layers must be rebuilt. Ordering instructions from least to most frequently changed maximises cache hits.
flowchart TD
A[Dockerfile Instruction] --> B{Has Layer<br/>Changed?}
B -->|No| C[Use Cached Layer]
B -->|Yes| D[Rebuild Layer]
D --> E[Invalidate All<br/>Subsequent Layers]
C --> F[Continue Build]
E --> F
style C fill:#e1ffe1
style D fill:#ffe1e1
style E fill:#ffe1e1
Layer Ordering Best Practice
FROM node:20-alpine
WORKDIR /app
# 1. Copy dependency manifests first (changes infrequently)
COPY package*.json ./
# 2. Install dependencies (cached unless manifests change)
RUN npm ci --only=production
# 3. Copy source code last (changes frequently)
COPY . .
# 4. Build application
RUN npm run build
CMD ["node", "dist/main.js"]
BuildKit Cache Mounts
BuildKit provides cache mounts that persist between builds, perfect for package managers.
# syntax=docker/dockerfile:1.4
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
# Mount go module cache
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
# Mount both module and build caches
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o app .
FROM alpine:3.19
COPY --from=builder /app/app /app
CMD ["/app"]
Python Package Cache
# syntax=docker/dockerfile:1.4
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
# Mount pip cache directory
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "app.py"]
npm/yarn Cache
# syntax=docker/dockerfile:1.4
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# Mount npm cache
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
COPY . .
CMD ["node", "index.js"]
apt/apk Package Manager Cache
# syntax=docker/dockerfile:1.4
FROM ubuntu:22.04
# Mount apt 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 \
build-essential \
python3-dev
# For Alpine
FROM alpine:3.19
RUN --mount=type=cache,target=/var/cache/apk \
apk --update add \
gcc \
musl-dev
Build Cache Configuration
# Enable BuildKit (for Docker < 23.0)
export DOCKER_BUILDKIT=1
# Build with inline cache
docker build \
--cache-from myapp:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t myapp:latest .
# Use external cache (registry)
docker buildx build \
--cache-from type=registry,ref=myregistry.com/myapp:cache \
--cache-to type=registry,ref=myregistry.com/myapp:cache,mode=max \
-t myapp:latest .
# Use local cache directory
docker buildx build \
--cache-from type=local,src=/tmp/cache \
--cache-to type=local,dest=/tmp/cache,mode=max \
-t myapp:latest .
Base Image Selection and Hardening
Choosing the right base image affects size, security, and compatibility. Hardening reduces attack surface and vulnerabilities.
Key Concepts
Base images range from full operating systems (Ubuntu, Debian) to minimal distributions (Alpine, distroless). Smaller bases reduce attack surface but may lack tools for debugging. Hardening includes removing unnecessary packages, running as non-root, and using immutable tags.
Base Image Comparison
| Base Image | Size | Use Case | Considerations |
|---|---|---|---|
scratch |
0MB | Static binaries | No shell, debugging tools, or libraries |
distroless |
~3MB (static) / ~50MB (python, java) | Go (static), Python/Java (runtime variants) | No shell, package manager; enhanced security |
alpine |
~7MB | Most languages | musl libc (not glibc); some compatibility issues |
debian:slim |
~80MB | Broad compatibility | Good balance of size and features |
ubuntu |
~78MB | Full-featured | Larger but most compatible |
Distroless Images (Google)
# Go application with distroless
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o app .
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/app /app
USER nonroot:nonroot
ENTRYPOINT ["/app"]
Using Scratch for Static Binaries
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
# Build fully static binary
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags="-w -s -extldflags '-static'" \
-o app .
# Absolute minimal image
FROM scratch
# Copy CA certificates for HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy binary
COPY --from=builder /app/app /app
# Define user in /etc/passwd
COPY <<EOF /etc/passwd
nobody:x:65534:65534:nobody:/nonexistent:/sbin/nologin
EOF
USER nobody
ENTRYPOINT ["/app"]
Alpine Hardening
FROM alpine:3.19
# Update packages and remove cache
RUN apk update && \
apk upgrade && \
apk add --no-cache \
ca-certificates \
tzdata && \
rm -rf /var/cache/apk/*
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["/app/entrypoint.sh"]
Debian Slim Hardening
FROM debian:12-slim
# Install only necessary packages
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Remove setuid/setgid bits for security
RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true
# Create non-root user
RUN useradd -m -u 1000 -s /bin/bash appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["./app"]
Security Best Practices
FROM python:3.12-slim
# Update base packages
RUN apt-get update && \
apt-get upgrade -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create user with no login shell
RUN useradd --no-log-init -r -u 1000 -m -s /sbin/nologin appuser
WORKDIR /app
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Copy application
COPY --chown=appuser:appuser . .
# Set read-only filesystem (where possible)
RUN chmod -R 555 /app
# Drop to non-root user
USER appuser
# Use ENTRYPOINT for immutability
ENTRYPOINT ["python", "-u", "app.py"]
Using Specific Image Tags
# ❌ Avoid: floating tags
FROM python:3
FROM node:latest
FROM alpine
# ✅ Good: specific version tags
FROM python:3.12.1-slim-bookworm
FROM node:20.11.0-alpine3.19
FROM alpine:3.19.0
# ✅ Best: digest pinning (immutable)
FROM python@sha256:af4e85f1cac90dd3771e47292ea7c8a9830abfabbe4faa5c53f158854c2e819e
# Combine for readability and immutability
FROM python:3.12.1-slim-bookworm@sha256:af4e85f1cac90dd3771e47292ea7c8a9830abfabbe4faa5c53f158854c2e819e
Layer Minimisation and .dockerignore
Minimising layers reduces image size and complexity. The .dockerignore file prevents unnecessary files from bloating images.
Key Concepts
Each RUN, COPY, and ADD instruction creates a layer. Combining commands reduces layers. Files added then deleted still exist in intermediate layers, wasting space. Use .dockerignore to exclude files before they're copied.
flowchart TB
subgraph "Without Layer Optimisation"
A1[RUN apt update] --> A2[RUN apt install curl]
A2 --> A3[RUN apt clean]
A3 --> A4[3 Layers<br/>Large Size]
end
subgraph "With Layer Optimisation"
B1[RUN apt update && install && clean] --> B2[1 Layer<br/>Small Size]
end
style A4 fill:#ffe1e1
style B2 fill:#e1ffe1
Combining RUN Commands
# ❌ Poor: Multiple layers, cache retained
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
# ✅ Good: Single layer, cache cleared
FROM ubuntu:22.04
RUN apt-get update && \
apt-get install -y \
curl \
vim \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
Multi-line Commands for Readability
FROM alpine:3.19
RUN apk add --no-cache \
python3 \
py3-pip \
git \
openssh-client \
ca-certificates \
&& python3 -m pip install --upgrade pip \
&& rm -rf /var/cache/apk/* \
&& rm -rf /tmp/*
# Alternative: heredoc for complex scripts (BuildKit)
RUN <<EOF
apk add --no-cache python3 py3-pip
python3 -m pip install --upgrade pip
adduser -D appuser
mkdir -p /app
chown appuser:appuser /app
EOF
.dockerignore Patterns
# Version control
.git
.gitignore
.gitattributes
# Build outputs
dist/
build/
*.pyc
__pycache__/
node_modules/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# Documentation
README.md
*.md
docs/
# Test files
tests/
test/
**/*_test.go
*.test
coverage/
# CI/CD
.github/
.gitlab-ci.yml
Jenkinsfile
# Environment and secrets
.env
.env.*
*.key
*.pem
secrets/
# Logs and temporary files
*.log
logs/
tmp/
temp/
# OS files
.DS_Store
Thumbs.db
# Large data files
*.zip
*.tar.gz
*.sql
datasets/
Advanced .dockerignore Patterns
# Include exceptions with !
**/*.md
!README.md
# Exclude all except specific files
*
!src/
!package.json
!package-lock.json
# Exclude patterns in subdirectories
**/secrets
**/temp
**/.cache
# Exclude by filetype
**/*.log
**/*.bak
**/*.tmp
Efficient COPY Operations
FROM node:20-alpine
WORKDIR /app
# ✅ Copy only necessary files first
COPY package*.json ./
RUN npm ci --only=production
# ✅ Copy source after dependencies
COPY src/ ./src/
COPY public/ ./public/
# ❌ Avoid: copying everything early
# COPY . . # This invalidates cache frequently
Reducing Layer Size Example
# ❌ Bad: Large layers with unnecessary files
FROM ubuntu:22.04
COPY . /app
RUN apt-get update && apt-get install -y build-essential
RUN cd /app && make
RUN apt-get remove -y build-essential
# ✅ Good: Clean up in same layer
FROM ubuntu:22.04
COPY src/ /app/src/
RUN apt-get update && \
apt-get install -y build-essential && \
cd /app && make && \
apt-get remove -y build-essential && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
BuildKit Features
BuildKit is Docker's enhanced build engine with parallel execution, advanced caching, and secret management.
Key Concepts
BuildKit enables concurrent build stages, mount types for secrets and caches, and efficient output formats. It's the default in Docker 23.0+ but can be enabled in earlier versions with DOCKER_BUILDKIT=1.
flowchart LR
subgraph "Traditional Build"
A1[Stage 1] --> A2[Stage 2] --> A3[Stage 3]
end
subgraph "BuildKit Parallel Build"
B1[Stage 1] --> B3[Stage 3]
B2[Stage 2] --> B3
end
style B3 fill:#e1ffe1
Enabling BuildKit
# Enable for single build
DOCKER_BUILDKIT=1 docker build -t myapp .
# Enable permanently (Docker < 23.0)
echo '{ "features": { "buildkit": true } }' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
# Using buildx (BuildKit CLI)
docker buildx build -t myapp .
# Create and use custom builder
docker buildx create --name mybuilder --use
docker buildx inspect --bootstrap
Dockerfile Syntax Declaration
# syntax=docker/dockerfile:1.4
FROM alpine:3.19
# Now BuildKit features are available
RUN --mount=type=cache,target=/var/cache/apk \
apk add python3
Secret Mounts
# syntax=docker/dockerfile:1.4
FROM python:3.12-slim
WORKDIR /app
# Mount secrets (not stored in image layers)
RUN --mount=type=secret,id=pip_config \
pip install --config /run/secrets/pip_config -r requirements.txt
# Alternative: GitHub token for private repos
RUN --mount=type=secret,id=github_token \
git clone https://$(cat /run/secrets/github_token)@github.com/org/repo.git
# Build with secrets
docker buildx build \
--secret id=pip_config,src=$HOME/.pip/pip.conf \
--secret id=github_token,env=GITHUB_TOKEN \
-t myapp .
SSH Agent Forwarding
# syntax=docker/dockerfile:1.4
FROM alpine:3.19
RUN apk add --no-cache git openssh-client
# Forward SSH agent for git operations
RUN --mount=type=ssh \
git clone git@github.com:private/repo.git /app
# Build with SSH forwarding
docker buildx build --ssh default -t myapp .
Bind Mounts for Build Context
# syntax=docker/dockerfile:1.4
FROM golang:1.21-alpine
WORKDIR /app
# Mount source code without copying
RUN --mount=type=bind,target=/app,rw \
--mount=type=cache,target=/go/pkg/mod \
go build -o /output/app .
FROM alpine:3.19
COPY --from=0 /output/app /app
CMD ["/app"]
Multi-platform Builds
# Build for multiple architectures
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
-t myapp:latest \
--push .
# Create multi-arch manifest
docker buildx imagetools create \
-t myapp:latest \
myapp:latest-amd64 \
myapp:latest-arm64
BuildKit Configuration
# syntax=docker/dockerfile:1.4
FROM alpine:3.19
# Parallel execution example
FROM golang:1.21 AS build-backend
WORKDIR /backend
COPY backend/ .
RUN go build -o app .
FROM node:20-alpine AS build-frontend
WORKDIR /frontend
COPY frontend/ .
RUN npm ci && npm run build
# Both stages run in parallel
FROM nginx:alpine
COPY --from=build-frontend /frontend/dist /usr/share/nginx/html
COPY --from=build-backend /backend/app /app
Output Types
# Standard image output
docker buildx build -t myapp:latest .
# Export to local filesystem
docker buildx build -o type=local,dest=./output .
# Export to tar
docker buildx build -o type=tar,dest=myapp.tar .
# Export OCI format
docker buildx build -o type=oci,dest=myapp-oci .
# Registry push without loading locally
docker buildx build --push -t registry.example.com/myapp:latest .
Supply Chain Security
Supply chain security ensures image integrity, provenance, and transparency through signing, attestation, and Software Bill of Materials (SBOM).
Key Concepts
Container supply chain security addresses risks from base images, dependencies, and build processes. Image signing verifies authenticity, SBOM provides dependency transparency, and attestations prove build provenance.
flowchart TB
A[Source Code] --> B[Build Process]
B --> C[Container Image]
C --> D[Vulnerability Scan]
D --> E[Generate SBOM]
E --> F[Image Signing]
F --> G[Attestation]
G --> H[Secure Registry]
H --> I[Signature Verification]
I --> J[Deploy]
style D fill:#fff4e1
style E fill:#fff4e1
style F fill:#e1ffe1
style I fill:#e1ffe1
Docker Content Trust (DCT)
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Generate keys (first time)
docker trust key generate mykey
# Add signer to repository
docker trust signer add --key mykey.pub myname myrepo/myapp
# Sign and push image
docker push myrepo/myapp:latest
# Pull with verification
docker pull myrepo/myapp:latest
# View signatures
docker trust inspect --pretty myrepo/myapp:latest
Notary for Image Signing
# Initialize repository with Notary
notary init registry.example.com/myapp
# Add signed tag
notary add registry.example.com/myapp latest sha256:abc123...
# Publish signatures
notary publish registry.example.com/myapp
# List signatures
notary list registry.example.com/myapp
# Verify signature
notary verify registry.example.com/myapp latest
Cosign (Sigstore)
# Install cosign
curl -sSL https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 \
-o /usr/local/bin/cosign
chmod +x /usr/local/bin/cosign
# Generate key pair
cosign generate-key-pair
# Sign image
cosign sign --key cosign.key registry.example.com/myapp:latest
# Verify signature
cosign verify --key cosign.pub registry.example.com/myapp:latest
# Keyless signing (with OIDC)
cosign sign registry.example.com/myapp:latest
# Verify keyless signature
cosign verify \
--certificate-identity=user@example.com \
--certificate-oidc-issuer=https://github.com/login/oauth \
registry.example.com/myapp:latest
Generate SBOM with Syft
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
# Generate SBOM from image
syft packages registry.example.com/myapp:latest -o spdx-json > sbom.spdx.json
# Generate SBOM from Dockerfile
syft packages dir:. -o cyclonedx-json > sbom.cyclonedx.json
# Generate multiple formats
syft packages myapp:latest -o json -o table -o spdx
# Scan local image
docker build -t myapp:latest .
syft packages myapp:latest
Attach SBOM with Cosign
# Generate SBOM
syft packages myapp:latest -o spdx-json > sbom.spdx.json
# Attach SBOM to image
cosign attach sbom --sbom sbom.spdx.json myapp:latest
# Sign the SBOM
cosign sign --key cosign.key $(cosign triangulate myapp:latest --type sbom)
# Verify and download SBOM
cosign verify-attestation --key cosign.pub myapp:latest
# Download SBOM
cosign download sbom myapp:latest > downloaded-sbom.json
BuildKit Attestations
# syntax=docker/dockerfile:1.4
FROM alpine:3.19
RUN apk add --no-cache python3
COPY app.py /app.py
CMD ["python3", "/app.py"]
# Build with SBOM attestation
docker buildx build \
--sbom=true \
--provenance=true \
-t myapp:latest \
--push .
# Build with custom attestations
docker buildx build \
--attest type=sbom,generator=syft \
--attest type=provenance,mode=max \
-t myapp:latest \
--push .
# View attestations
docker buildx imagetools inspect myapp:latest --format "{{json .SBOM}}"
Vulnerability Scanning
# Scan with Trivy
trivy image myapp:latest
# Scan with severity filter
trivy image --severity HIGH,CRITICAL myapp:latest
# Scan and exit on vulnerabilities
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Generate vulnerability report
trivy image -f json -o report.json myapp:latest
# Scan filesystem
trivy fs --security-checks vuln,config .
# Scan Kubernetes manifests
trivy config kubernetes/
Grype for Vulnerability Scanning
# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh
# Scan image
grype registry.example.com/myapp:latest
# Scan with specific severity
grype myapp:latest --fail-on high
# Output formats
grype myapp:latest -o json > vulnerabilities.json
grype myapp:latest -o table
grype myapp:latest -o cyclonedx > vulns.cyclonedx.xml
Complete Supply Chain Example
#!/bin/bash
set -e
IMAGE="registry.example.com/myapp:latest"
# 1. Build image with attestations
echo "Building image..."
docker buildx build \
--sbom=true \
--provenance=true \
--platform linux/amd64,linux/arm64 \
-t $IMAGE \
--push .
# 2. Generate detailed SBOM
echo "Generating SBOM..."
syft packages $IMAGE -o spdx-json > sbom.spdx.json
# 3. Scan for vulnerabilities
echo "Scanning for vulnerabilities..."
trivy image --severity HIGH,CRITICAL $IMAGE
# 4. Sign image
echo "Signing image..."
cosign sign --key cosign.key $IMAGE
# 5. Attach and sign SBOM
echo "Attaching SBOM..."
cosign attach sbom --sbom sbom.spdx.json $IMAGE
cosign sign --key cosign.key $(cosign triangulate $IMAGE --type sbom)
# 6. Create attestation
echo "Creating attestation..."
cosign attest --key cosign.key \
--predicate sbom.spdx.json \
--type spdxjson \
$IMAGE
echo "Supply chain security complete!"
Policy Enforcement with OPA/Gatekeeper
# Require signed images
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: requireimagesignature
spec:
crd:
spec:
names:
kind: RequireImageSignature
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package requireimagesignature
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not signature_verified(container.image)
msg := sprintf("Image %v is not signed", [container.image])
}
signature_verified(image) {
# Integration with cosign/notary verification
}
Quick Reference
Image Size Reduction Checklist
| Technique | Expected Reduction | Priority |
|---|---|---|
| Multi-stage builds | 50-90% | High |
| Alpine/distroless base | 30-80% | High |
| .dockerignore configuration | 10-40% | High |
| Combine RUN commands | 5-15% | Medium |
| Remove package caches | 5-20% | Medium |
| Use specific COPY paths | 5-10% | Medium |
Strip binaries (-ldflags "-w -s") |
20-30% for Go | Low |
Build Performance Optimisation
# Warm up cache
docker buildx build --cache-to type=local,dest=/tmp/cache .
# Use cached build
docker buildx build --cache-from type=local,src=/tmp/cache .
# Parallel multi-platform builds
docker buildx build --platform linux/amd64,linux/arm64 .
# Enable BuildKit features
DOCKER_BUILDKIT=1 docker build .
Security Hardening Commands
# Non-root user
RUN useradd -r -u 1000 -m -s /sbin/nologin appuser
USER appuser
# Read-only root filesystem
COPY --chown=appuser:appuser . /app
RUN chmod -R 555 /app
# Drop capabilities (runtime)
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp
# Security scanning
trivy image --severity CRITICAL myapp:latest
grype myapp:latest --fail-on critical
Supply Chain Security Workflow
# 1. Build with attestations
docker buildx build --sbom=true --provenance=true -t myapp:latest .
# 2. Generate SBOM
syft packages myapp:latest -o spdx-json > sbom.json
# 3. Scan vulnerabilities
trivy image myapp:latest
# 4. Sign image
cosign sign --key cosign.key myapp:latest
# 5. Verify signature
cosign verify --key cosign.pub myapp:latest
Common Issues and Solutions
Issue: Large Image Sizes
Symptoms: Images exceeding hundreds of MB or GB, slow push/pull times
Solutions:
# ✅ Use multi-stage builds
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine # Much smaller
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/main.js"]
# ✅ Use minimal base images
FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/dist ./dist
CMD ["dist/main.js"]
# ✅ Clean up in the same layer
RUN apt-get update && \
apt-get install -y package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
Issue: Slow Build Times
Symptoms: Builds taking minutes, poor cache utilization
Solutions:
# ✅ Order layers from least to most frequently changed
FROM node:20-alpine
WORKDIR /app
# Dependencies first (cached unless changed)
COPY package*.json ./
RUN npm ci
# Source code last
COPY . .
RUN npm run build
# ✅ Use BuildKit cache mounts
# syntax=docker/dockerfile:1.4
RUN --mount=type=cache,target=/root/.npm \
npm ci
# Use BuildKit
export DOCKER_BUILDKIT=1
# External cache
docker buildx build \
--cache-from type=registry,ref=myapp:cache \
--cache-to type=registry,ref=myapp:cache,mode=max \
-t myapp:latest .
Issue: Secrets in Image Layers
Symptoms: Credentials, tokens, or keys visible in image history
Solutions:
# ❌ Never do this
RUN git clone https://token@github.com/org/repo.git
# ✅ Use BuildKit secret mounts
# syntax=docker/dockerfile:1.4
RUN --mount=type=secret,id=github_token \
git clone https://$(cat /run/secrets/github_token)@github.com/org/repo.git
# ✅ Use SSH forwarding
RUN --mount=type=ssh \
git clone git@github.com:org/repo.git
# Build with secrets
docker buildx build \
--secret id=github_token,env=GITHUB_TOKEN \
--ssh default \
.
Issue: Cache Invalidation
Symptoms: Cache not being used, every build starts from scratch
Solutions:
# ❌ Poor: This invalidates cache frequently
COPY . .
RUN npm install
# ✅ Good: Copy dependency files first
COPY package*.json ./
RUN npm ci
COPY . . # Only invalidates if source changes
# Use explicit cache sources
docker build --cache-from myapp:latest -t myapp:latest .
# Check what's invalidating cache
docker build --progress=plain -t myapp:latest .
Issue: Multi-platform Build Failures
Symptoms: Builds fail for ARM architectures, QEMU errors
Solutions:
# Install QEMU emulators
docker run --privileged --rm tonistiigi/binfmt --install all
# Create multi-platform builder
docker buildx create --name multiarch --use
docker buildx inspect --bootstrap
# Build with specific platforms
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest \
--push .
Issue: BuildKit Not Available
Symptoms: --mount not recognized, cache features unavailable
Solutions:
# Enable BuildKit (Docker < 23.0)
export DOCKER_BUILDKIT=1
# Permanent enablement
cat <<EOF | sudo tee /etc/docker/daemon.json
{
"features": {
"buildkit": true
}
}
EOF
sudo systemctl restart docker
# Use buildx
docker buildx version
Issue: Vulnerability Scan Failures
Symptoms: Images rejected due to critical vulnerabilities
Solutions:
# Scan before building
trivy config .
# Update base image
FROM python:3.12-slim # Use latest patch version
# Upgrade packages
RUN apt-get update && apt-get upgrade -y
# Filter false positives
trivy image --ignore-unfixed myapp:latest
# Use minimal base images (fewer vulnerabilities)
FROM gcr.io/distroless/python3-debian12
Issue: Unsigned Images Rejected
Symptoms: Deployment fails due to missing signatures
Solutions:
# Sign images automatically in CI
cosign sign --key cosign.key ${IMAGE}
# Verify in deployment pipeline
cosign verify --key cosign.pub ${IMAGE}
# Use keyless signing
cosign sign ${IMAGE} # Uses OIDC
# Integrate with admission controllers
kubectl apply -f policy-controller.yaml
Issue: SBOM Generation Errors
Symptoms: Missing dependencies in SBOM, incomplete data
Solutions:
# Use multiple SBOM generators
syft packages myapp:latest -o spdx-json > syft-sbom.json
trivy image --format spdx-json -o trivy-sbom.json myapp:latest
# Include build attestations
docker buildx build \
--sbom=true \
--attest type=sbom,generator=syft \
-t myapp:latest .
# Generate from source and image
syft packages dir:. -o cyclonedx-json > source-sbom.json
syft packages myapp:latest -o cyclonedx-json > image-sbom.json
Related Topics
Now that you've mastered container image optimisation, consider exploring these related areas:
- Docker - Fundamental container commands and operations
- Kubernetes - Deploying optimised images in production clusters
- Container Security - Advanced security scanning and policy enforcement
- CI/CD Patterns - Integrating image optimisation into pipelines
- Container Registries - Managing and distributing optimised images
- BuildKit - Deep dive into advanced build features