Best Practices for Dockerfile Multi-Stage Builds
Learn best practices for Dockerfile multi-stage builds: reduce image size, optimize caching, secure build stages, and improve build performance.
Secure Docker Multi-Stage Builds Guide
A comprehensive security-focused guide for implementing multi-stage builds in Docker.
Multi-stage builds are a powerful feature in Docker that allow you to create smaller, more secure container images by separating the build environment from the runtime environment. This guide covers best practices for implementing multi-stage builds with a strong focus on security considerations.
1. Table of Contents
- 1. Table of Contents
- 2. Introduction to Multi-Stage Builds
- 3. Docker Installation and Verification
- 4. Multi-Stage Build Fundamentals
- 5. Security Best Practices
- 6. Advanced Configuration
- 7. Production Hardening Techniques
- 8. Secure CI/CD Integration
- 9. Testing and Validation
- 10. References and Resources
- 11. Appendices
2. Introduction to Multi-Stage Builds
Multi-stage builds were introduced in Docker 17.05 to address common challenges in containerization: bloated images, security vulnerabilities, and complex build processes. By using multiple stages within a single Dockerfile, you can separate build-time dependencies from runtime dependencies, resulting in smaller and more secure container images.
2.1 Security Benefits
Multi-stage builds enhance security by:
- Reducing the attack surface by eliminating build tools and dependencies from the final image
- Minimizing the number of vulnerable packages in your runtime environment
- Facilitating separation of concerns between build and runtime environments
- Enabling more granular control over what gets included in the final image
2.2 Performance Advantages
Beyond security, multi-stage builds provide:
- Smaller image sizes, reducing storage costs and network transfer times
- Faster container startup times
- Improved caching for more efficient rebuilds
- Simplified CI/CD pipelines
3. Docker Installation and Verification
Before implementing multi-stage builds, you need a properly installed and configured Docker environment. The following are secure installation procedures for various platforms.
3.1 Linux Installation
For Debian/Ubuntu systems:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Remove old versions if present
sudo apt-get remove docker docker-engine docker.io containerd runc
# Set up repository
sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl gnupg lsb-release
# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# Set up stable repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# Add your user to the docker group (optional, but recommended)
sudo usermod -aG docker $USER
# Log out and log back in for this to take effect
3.2 macOS Installation
For macOS, use Docker Desktop:
- Download Docker Desktop from the official Docker website
- Verify the checksum:
1
2
shasum -a 256 ~/Downloads/Docker.dmg
# Compare with the checksum published on Docker's website
- Install Docker Desktop by dragging it to your Applications folder
- Start Docker Desktop and complete the setup process
3.3 Windows Installation
For Windows, use Docker Desktop with WSL 2 backend for improved security and performance:
- Enable WSL 2 by following Microsoft’s official documentation
- Download Docker Desktop from the official Docker website
- Verify the installer using PowerShell:
1
2
Get-FileHash -Algorithm SHA256 -Path .\DockerDesktopInstaller.exe
# Compare with the checksum published on Docker's website
- Run the installer and ensure “Use WSL 2 instead of Hyper-V” is selected
- Complete the installation and restart if prompted
3.4 Installation Verification
After installation, verify Docker is functioning correctly and securely:
1
2
3
4
5
6
7
8
9
10
11
# Check Docker version
docker --version
# Verify daemon is running
docker info
# Run a test container
docker run --rm hello-world
# Check for any warnings or security issues
docker info | grep -i warning
4. Multi-Stage Build Fundamentals
4.1 Basic Syntax and Structure
A multi-stage Dockerfile uses multiple FROM
statements, each starting a new build stage:
1
2
3
4
5
6
7
8
9
10
11
12
13
# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
This example shows a typical web application build, using Node.js to build the application and Nginx to serve it.
4.2 Stage Naming Conventions
Use clear, descriptive names for your build stages:
1
2
3
4
5
6
7
8
FROM golang:1.17 AS builder
# Build stage operations...
FROM alpine:3.14 AS security-scanner
# Security scanning operations...
FROM gcr.io/distroless/static AS runtime
# Final stage operations...
Consistent naming conventions improve readability and maintainability. Consider a prefix system for different types of stages:
build-*
for compilation stagestest-*
for testing stagesscan-*
for security scanning stagesfinal
orruntime
for the production image
4.3 Copying Between Stages
You can selectively copy artifacts between stages using the COPY --from=<stage>
directive:
1
2
3
4
5
6
7
8
# Copy entire directories
COPY --from=builder /app/bin /app
# Copy specific files
COPY --from=builder /app/bin/myapp /usr/local/bin/
# Copy from a specific stage using its index (less recommended, as it's brittle)
COPY --from=0 /app/config.json /config.json
Best practices for copying between stages:
- Be explicit about what files you’re copying
- Copy only what you need for the runtime
- Use named stages instead of numeric indices
- Consider using checksums for verifying copied artifacts
5. Security Best Practices
5.1 Base Image Selection
Choose secure, minimal, and regularly updated base images:
1
2
3
4
5
6
7
8
9
10
11
# Prefer official images
FROM python:3.10-slim
# Or distroless images for even smaller attack surface
FROM gcr.io/distroless/python3
# Avoid using :latest tag - pin specific versions
FROM node:16.13.1-alpine3.14
# Use specific digests for immutability
FROM debian@sha256:f5676abb29a3c6a0c50046069d862ebb6f91c8c1b3e4dc12dbd02f3f03b3cdf7
Security considerations:
- Minimal images: Alpine, slim variants, or distroless images contain fewer packages
- Official images: Maintained by the Docker team or the software’s maintainers
- Version pinning: Use specific versions or SHA digests for reproducibility
- Scan before use: Validate images with security scanners before using them
5.2 Minimal Runtime Images
Keep your final image as small as possible by including only what’s needed to run your application:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Build stage
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Runtime stage with minimal JRE
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# Specify exact version of dependencies
RUN apk add --no-cache tzdata=2021e-r0
USER nobody:nobody
ENTRYPOINT ["java", "-jar", "app.jar"]
Security benefits of minimal images:
- Fewer packages means fewer potential vulnerabilities
- Smaller images reduce the attack surface
- Faster scanning and deployment
- Easier to understand and audit what’s in the image
5.3 User Permissions
Avoid running containers as root:
1
2
3
4
5
6
7
8
9
10
11
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Set ownership of application files
COPY --from=builder --chown=appuser:appgroup /app/myapp /app/
# Switch to the non-root user
USER appuser
# Make sure the application can run as non-root
ENTRYPOINT ["/app/myapp"]
For distroless images that don’t have shell access:
1
2
3
4
5
6
7
8
9
# Use the nonroot user that comes with distroless
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /go/bin/app /app
# Use the nonroot user (uid=65532)
USER nonroot:nonroot
ENTRYPOINT ["/app"]
5.4 Secrets Management
Never include secrets in your Docker images:
Bad practice (DO NOT DO THIS):
1
2
# DON'T DO THIS - Secrets in Dockerfile
ENV API_KEY="secret-api-key-value"
Better approaches:
- Use Docker BuildKit’s secret mounting (available since Docker 18.09):
1
2
3
# syntax=docker/dockerfile:1.2
FROM alpine
RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret
Build with:
1
DOCKER_BUILDKIT=1 docker build --secret id=mysecret,src=./secret.txt .
- Use multi-stage builds to prevent secrets from being stored in layers:
1
2
3
4
5
6
7
FROM alpine AS builder
ARG TEMP_API_KEY
RUN echo "ApiKey=${TEMP_API_KEY}" >> /app/config.temp
FROM alpine
COPY --from=builder /app/config.temp /app/config
# The API key is not stored in an image layer
- Use environment variables at runtime:
1
2
FROM alpine
CMD ["sh", "-c", "echo Using API key: $API_KEY"]
Run with:
1
docker run --env API_KEY=value myimage
5.5 Image Scanning
Integrate scanning directly in your multi-stage builds:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY . .
RUN npm ci && npm run build
# Security scanning stage
FROM aquasec/trivy:latest AS security-scan
COPY --from=build /app /app
RUN trivy fs --exit-code 1 --severity HIGH,CRITICAL /app
# Production stage (only proceeds if scan passes)
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
Best practices for image scanning:
- Scan both your base images and your application code
- Set appropriate severity thresholds based on your risk tolerance
- Integrate scanning into your CI/CD pipeline
- Keep vulnerability databases updated
- Implement policies for addressing discovered vulnerabilities
5.6 Build-Time vs. Runtime Variables
Separate build-time variables from runtime configuration:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Build stage with build arguments
FROM node:16-alpine AS build
ARG NODE_ENV=production
ARG BUILD_VERSION
WORKDIR /app
COPY . .
RUN echo "Building version: $BUILD_VERSION with NODE_ENV=$NODE_ENV" && \
npm ci && \
npm run build
# Runtime stage with environment variables
FROM nginx:alpine
ENV API_URL="https://api.example.com"
ENV LOG_LEVEL="info"
COPY --from=build /app/dist /usr/share/nginx/html
Security considerations:
- Use
ARG
for build-time variables (these don’t persist in the final image) - Use
ENV
for runtime configuration - Consider externalizing sensitive configuration using Docker secrets or config maps
- Don’t use build arguments for secrets
- Document which environment variables are required at runtime
6. Advanced Configuration
6.1 Multi-Architecture Builds
Create images that work across different CPU architectures:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Use BuildKit's architecture-specific build capability
FROM --platform=$BUILDPLATFORM golang:1.17-alpine AS builder
ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building on $BUILDPLATFORM for $TARGETPLATFORM"
WORKDIR /app
COPY . .
# Set architecture-specific build flags
RUN case "$TARGETPLATFORM" in \
"linux/amd64") GOARCH=amd64 ;; \
"linux/arm64") GOARCH=arm64 ;; \
"linux/arm/v7") GOARCH=arm ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
esac && \
GOOS=linux go build -o myapp
FROM --platform=$TARGETPLATFORM alpine
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["/usr/local/bin/myapp"]
Build for multiple platforms:
1
docker buildx build --platform linux/amd64,linux/arm64 -t myorg/myapp:latest .
Security aspects of multi-architecture builds:
- Ensure your security measures work across all targeted architectures
- Verify packages and dependencies for each architecture
- Test security controls on all supported platforms
- Consider platform-specific vulnerabilities
6.2 Optimizing Layer Caching
Arrange your Dockerfile commands to maximize cache efficiency while maintaining security:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
FROM node:16-alpine AS builder
# Copy package files first to leverage caching
WORKDIR /app
COPY package.json package-lock.json ./
# Install dependencies (cached if package files don't change)
RUN npm ci
# Copy source code (changes more frequently)
COPY . .
# Build the application
RUN npm run build
# Use a clean image for production
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
Security considerations for layer caching:
- Don’t sacrifice security for build speed
- Remember that any RUN instruction creates a new layer that becomes part of the image history
- Consider using multi-stage builds to discard intermediate layers with potentially sensitive data
- Use
.dockerignore
to prevent unnecessary files from being included
Example .dockerignore
file:
1
2
3
4
5
6
.git
node_modules
*.log
.env*
secrets/
tests/
6.3 Dockerfile Linting and Analysis
Use automated tools to detect issues in your Dockerfiles:
Example integration in a multi-stage build:
1
2
3
4
5
6
7
8
9
10
11
12
FROM hadolint/hadolint:latest AS dockerfile-lint
COPY Dockerfile /
RUN hadolint /Dockerfile
FROM golang:1.17-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
FROM alpine:3.14
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["/usr/local/bin/myapp"]
Run linting as part of your CI/CD pipeline:
1
docker run --rm -i hadolint/hadolint < Dockerfile
6.4 Common Misconfigurations
Avoid these common security mistakes in multi-stage builds:
1. Unnecessary privileges:
1
2
3
4
5
6
7
8
# BAD: Running as root by default
FROM alpine
# No USER instruction means container runs as root
# GOOD: Specify a non-root user
FROM alpine
RUN adduser -D myuser
USER myuser
2. Copying more than needed:
1
2
3
4
5
# BAD: Copying everything from build stage
COPY --from=builder /app /app
# GOOD: Copy only what's needed
COPY --from=builder /app/bin/myapp /usr/local/bin/myapp
3. Not cleaning up:
1
2
3
4
5
6
7
8
9
# BAD: Leaving package manager caches
RUN apt-get update && \
apt-get install -y some-package
# GOOD: Clean up after installation
RUN apt-get update && \
apt-get install -y some-package && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
4. Using latest tag:
1
2
3
4
5
# BAD: Unpredictable base image
FROM ubuntu:latest
# GOOD: Pin to specific version
FROM ubuntu:20.04
5. Mixing build and runtime stages:
1
2
3
4
5
6
7
8
9
10
11
# BAD: Installing build tools in runtime image
FROM alpine
RUN apk add --no-cache build-base gcc python3-dev
# GOOD: Separate build and runtime concerns
FROM alpine AS builder
RUN apk add --no-cache build-base gcc python3-dev
# ...build steps...
FROM alpine
# ...copy only built artifacts from builder...
7. Production Hardening Techniques
7.1 Image Signing and Verification
Sign your images to ensure integrity in your supply chain:
1
2
3
4
5
6
7
8
# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Sign and push image
docker push myorg/myapp:1.0.0
# Verify signed image
docker trust inspect --pretty myorg/myapp:1.0.0
You can integrate this with your multi-stage builds by having a final stage that verifies signatures:
1
2
3
4
5
FROM alpine:3.14 AS verify
ARG IMAGE_NAME
ARG IMAGE_TAG
RUN apk add --no-cache notary && \
notary verify -s https://notary.docker.io $IMAGE_NAME $IMAGE_TAG
7.2 Content Trust
Configure Docker Content Trust for your build pipeline:
1
2
3
4
5
6
7
8
9
10
11
# In your CI/CD pipeline script
set -e
export DOCKER_CONTENT_TRUST=1
export DOCKER_CONTENT_TRUST_SERVER=https://notary.docker.io
# Import signing keys securely from your CI/CD secrets
docker trust key load /path/to/secured/keys
# Build and push with content trust enabled
docker build -t myorg/myapp:${VERSION} .
docker push myorg/myapp:${VERSION}
Security considerations:
- Manage signing keys securely
- Implement key rotation policies
- Consider a dedicated build server for signing
- Document verification procedures for downstream consumers
7.3 Runtime Constraints
Apply security constraints when running containers:
1
2
3
4
5
6
7
8
9
10
11
# Run with restricted capabilities
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myorg/myapp:1.0.0
# Set read-only filesystem
docker run --read-only myorg/myapp:1.0.0
# Set memory limits
docker run --memory=512m --memory-swap=512m myorg/myapp:1.0.0
# Apply seccomp profiles
docker run --security-opt seccomp=/path/to/seccomp.json myorg/myapp:1.0.0
Document these constraints in your README.md
or deployment manifests.
Example Docker Compose file with security constraints:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version: '3.8'
services:
webapp:
image: myorg/myapp:1.0.0
read_only: true
tmpfs:
- /tmp
- /var/run
security_opt:
- no-new-privileges
- seccomp=/path/to/seccomp.json
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
7.4 Logging and Audit
Implement proper logging for your containerized applications:
1
2
3
4
5
6
7
8
9
10
11
12
13
FROM alpine:3.14
# Configure logging
RUN mkdir -p /var/log/myapp && \
chmod 755 /var/log/myapp
# Use volumes for persistent logs
VOLUME /var/log/myapp
# Set up log rotation
COPY logrotate-config /etc/logrotate.d/myapp
# Your application should log to this directory
CMD ["/bin/sh", "-c", "exec myapp > /var/log/myapp/app.log 2>&1"]
For Docker deployment, configure logging drivers:
1
docker run --log-driver=json-file --log-opt max-size=10m --log-opt max-file=3 myorg/myapp:1.0.0
Security considerations for logging:
- Don’t log sensitive information
- Implement log rotation to prevent disk space issues
- Use structured logging when possible
- Consider using a centralized logging solution
- Define retention policies compliant with your regulatory requirements
8. Secure CI/CD Integration
8.1 Pipeline Configuration
Example GitLab CI/CD configuration for secure multi-stage builds:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
stages:
- lint
- build
- scan
- test
- deploy
variables:
DOCKER_BUILDKIT: 1
DOCKER_CONTENT_TRUST: 1
lint:
stage: lint
image: hadolint/hadolint:latest
script:
- hadolint Dockerfile
build:
stage: build
image: docker:20.10
services:
- docker:20.10-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --exit-code 1 --severity HIGH,CRITICAL $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
test:
stage: test
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
script:
- /app/run-tests.sh
deploy:
stage: deploy
image: docker:20.10
services:
- docker:20.10-dind
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
Security considerations:
- Use separate stages for building, testing, and scanning
- Implement branch protection rules
- Restrict who can merge to production branches
- Store credentials securely in CI/CD variables
- Implement approval workflows for production deployments
8.2 Automated Security Scanning
Integrate security scanning into your CI/CD pipeline:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scan-dependencies:
image: owasp/dependency-check
script:
- /usr/share/dependency-check/bin/dependency-check.sh --scan /app --format JSON --out dependency-check-report.json
artifacts:
paths:
- dependency-check-report.json
scan-image:
image: aquasec/trivy
script:
- trivy image --exit-code 0 --format json --output trivy-report.json $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
artifacts:
paths:
- trivy-report.json
scan-secrets:
image: zricethezav/gitleaks
script:
- gitleaks detect -v --source . --report-path=gitleaks-report.json
artifacts:
paths:
- gitleaks-report.json
Use centralized scanning results for compliance reporting.
8.3 Deployment Strategies
Implement secure deployment strategies:
1. Blue-Green Deployment:
1
2
3
4
5
6
7
8
9
10
# Deploy new version to staging
docker run -d --name app-green $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# Run tests against staging
./run-tests.sh app-green
# If tests pass, update production
docker stop app-blue
docker rm app-blue
docker run -d --name app-blue $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
2. Canary Deployment:
1
2
3
4
5
6
# Deploy new version to 10% of users
kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
kubectl scale deployment/myapp --replicas=10
kubectl scale deployment/myapp-canary --replicas=1
# Monitor and gradually increase traffic
Security considerations:
- Implement rollback procedures
- Monitor application health during deployment
- Use immutable infrastructure principles
- Implement proper access controls for deployment systems
- Document emergency rollback procedures
9. Testing and Validation
9.1 Security Testing Tools
Integrate these tools into your workflow:
- Clair: For container vulnerability scanning
- Trivy: For comprehensive vulnerability scanning
- Anchore: For deep image analysis
- Dagda: For runtime monitoring
- Falco: For runtime security monitoring
Example Trivy integration in a multi-stage build:
1
2
3
4
5
6
7
8
FROM aquasec/trivy:latest AS security-scanner
RUN mkdir /scan
COPY --from=builder /app /scan
RUN trivy fs --exit-code 1 --no-progress --severity HIGH,CRITICAL /scan
FROM alpine:3.14
COPY --from=builder /app/myapp /usr/local/bin/
CMD ["/usr/local/bin/myapp"]
9.2 Validation Scripts
Create scripts to validate your Docker images:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#!/bin/bash
# validate-image.sh
set -e
IMAGE="$1"
echo "Validating image: $IMAGE"
# Check for basic runtime capabilities
echo "Testing container startup..."
CONTAINER_ID=$(docker run -d $IMAGE)
sleep 2
CONTAINER_STATUS=$(docker inspect -f '{{.State.Status}}' $CONTAINER_ID)
if [ "$CONTAINER_STATUS" != "running" ]; then
echo "Container failed to start properly"
docker logs $CONTAINER_ID
docker rm -f $CONTAINER_ID
exit 1
fi
# Check for non-root user
echo "Checking for non-root user..."
USER_ID=$(docker exec $CONTAINER_ID id -u)
if [ "$USER_ID" == "0" ]; then
echo "WARNING: Container is running as root"
fi
# Check exposed ports
echo "Checking exposed ports..."
EXPOSED_PORTS=$(docker inspect -f '{{range $port, $_ := .Config.ExposedPorts}}{{$port}} {{end}}' $CONTAINER_ID)
echo "Exposed ports: $EXPOSED_PORTS"
# Clean up
docker rm -f $CONTAINER_ID
echo "Validation complete"
Run this script in your CI/CD pipeline:
1
./validate-image.sh myorg/myapp:latest
9.3 Compliance Checking
Implement compliance checks for your Docker images:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash
# check-compliance.sh
IMAGE="$1"
echo "Checking compliance for image: $IMAGE"
# Extract metadata
DOCKER_INFO=$(docker inspect $IMAGE)
# Check for required labels
if ! echo "$DOCKER_INFO" | grep -q '"org.label-schema.version"'; then
echo "FAIL: Missing required version label"
exit 1
fi
# Check for exposed ports
if echo "$DOCKER_INFO" | grep -q '"22/tcp"'; then
echo "FAIL: SSH port 22 should not be exposed"
exit 1
fi
# Check for read-only filesystem capability
if ! echo "$DOCKER_INFO" | grep -q '"ReadonlyRootfs": true'; then
echo "WARNING: Container does not use read-only filesystem"
fi
# Check for custom seccomp profile
if ! echo "$DOCKER_INFO" | grep -q '"SecurityOpt"'; then
echo "WARNING: No custom seccomp profile applied"
fi
echo "Compliance check complete"
10. References and Resources
Official Documentation
Security Guidance
- NIST Container Security Guide
- CIS Docker Benchmark
- OWASP Docker Security Cheat Sheet
- Docker Security Benchmark by Docker
- Snyk Container Security Best Practices
Tools
- Hadolint - Dockerfile linter
- Trivy - Vulnerability scanner for containers
- Docker Bench Security - Test Docker against best practices
- Anchore Engine - Container image inspection and analysis
- Clair - Vulnerability static analysis for containers
- Dockle - Container image linter
Blogs and Articles
- Multi-stage builds for creating minimal Docker images
- Using Docker Build Secrets Securely
- Container Security Best Practices
- Secrets in Docker - Best Practices
11. Appendices
11.1 Troubleshooting
Common Issues and Solutions
Issue | Solution |
---|---|
Build context too large | Use .dockerignore to exclude unnecessary files |
Image size too large | Implement multi-stage builds to reduce final image size |
Caching not working properly | Review layer order and optimize for cache efficiency |
Build fails in CI but works locally | Ensure CI environment has same Docker version and BuildKit settings |
Container can’t access network | Check network configuration and firewall rules |
Permission denied errors | Set proper file permissions or use appropriate user |
Debugging Build Issues
1
2
3
4
5
6
7
8
9
10
11
12
# Enable BuildKit verbose output
BUILDKIT_PROGRESS=plain docker build .
# Check image layers
docker history myorg/myapp:latest
# Inspect image configuration
docker inspect myorg/myapp:latest
# Run intermediate build stages for debugging
docker build --target builder -t debug-image .
docker run -it debug-image /bin/sh
11.2 Language-Specific Considerations
Java Applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Build stage
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
# Download dependencies first for better caching
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Runtime stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar
# Use specific JVM flags for containers
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0 -XX:+UseContainerSupport"
USER nobody:nobody
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
Security considerations:
- Use JRE instead of JDK in production
- Set appropriate memory limits
- Enable JVM container support
- Run as non-root user
Node.js Applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Build stage
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
# Run security audit
RUN npm audit --production
# Build application
RUN npm run build
# Runtime stage
FROM node:16-alpine
WORKDIR /app
# Create non-root user
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs
# Copy only production dependencies and built files
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
RUN npm ci --only=production
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
# Use non-root user
USER nodejs
# Set NODE_ENV
ENV NODE_ENV production
CMD ["node", "dist/index.js"]
Security considerations:
- Run
npm audit
during build - Use
npm ci
instead ofnpm install
- Set
NODE_ENV=production
- Don’t run as root
- Only install production dependencies in the final image
Python Applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# Build stage
FROM python:3.10-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc libffi-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt
# Runtime stage
FROM python:3.10-slim
WORKDIR /app
# Create non-root user
RUN useradd -m appuser
# Copy wheels from builder
COPY --from=builder /app/wheels /app/wheels
# Install dependencies from wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/app/wheels -r requirements.txt && \
rm -rf /app/wheels
# Copy application code
COPY --chown=appuser:appuser app /app/app
# Switch to non-root user
USER appuser
# Run with Python optimization
CMD ["python", "-O", "-m", "app"]
Security considerations:
- Use slim base images
- Separate build and runtime dependencies
- Use wheels for deterministic builds
- Run as non-root user
- Enable Python optimizations
Go Applications
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Build stage
FROM golang:1.17-alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git ca-certificates
# Copy go.mod and go.sum first for better caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build statically linked binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o app .
# Runtime stage - scratch is the most minimal image
FROM scratch
# Copy CA certificates for secure connections
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy the binary
COPY --from=builder /app/app /app
# Run as non-root (note: no shell in scratch image)
USER 1000:1000
ENTRYPOINT ["/app"]
Security considerations:
- Use static linking to avoid library dependencies
- Use scratch or distroless base images
- Include CA certificates for secure connections
- Strip debug information from binaries
- Run as non-root user
11.3 Glossary
Term | Definition |
---|---|
BuildKit | Docker’s next-generation build system with advanced caching, parallel building, and more secure features. |
Content Trust | A feature that ensures image integrity through digital signatures. |
Distroless | Minimal container images that contain only the application and its runtime dependencies, but not package managers or shells. |
Layer Caching | Docker’s mechanism for reusing existing layers when building images to speed up build time. |
Multi-Stage Build | A Docker feature that allows using multiple FROM statements in a Dockerfile, where each FROM instruction can use a different base. |
OCI | Open Container Initiative, an open governance structure for container formats and runtimes. |
Scratch Image | The most minimal Docker image possible, containing no files or programs. |
Seccomp | Secure Computing Mode, a Linux kernel feature that restricts the system calls that a process can make. |
SBOM | Software Bill of Materials, a list of components in a software artifact. |
Vulnerability Scanning | The process of identifying security vulnerabilities in container images. |
12. Quick Reference
Essential Multi-Stage Build Example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# Build stage
FROM node:16-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Test stage
FROM build AS test
RUN npm test
# Security scan stage
FROM aquasec/trivy:latest AS security-scan
COPY --from=build /app /app
RUN trivy fs --exit-code 1 --severity HIGH,CRITICAL /app
# Production stage
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
# Add security headers
RUN echo 'server {\n\
listen 80;\n\
server_name localhost;\n\
location / {\n\
root /usr/share/nginx/html;\n\
index index.html;\n\
try_files $uri $uri/ /index.html;\n\
add_header X-Content-Type-Options nosniff;\n\
add_header X-Frame-Options DENY;\n\
add_header X-XSS-Protection "1; mode=block";\n\
}\n\
}' > /etc/nginx/conf.d/default.conf
# Run as non-root
RUN chown -R nginx:nginx /usr/share/nginx/html && \
chown -R nginx:nginx /var/cache/nginx && \
chown -R nginx:nginx /var/log/nginx && \
touch /var/run/nginx.pid && \
chown -R nginx:nginx /var/run/nginx.pid
USER nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Security Checklist
- Use specific versions of base images, not
:latest
- Implement multi-stage builds to reduce attack surface
- Run containers as non-root users
- Use minimal or distroless base images for production
- Keep secrets out of images using BuildKit secrets
- Scan images for vulnerabilities
- Sign and verify images
- Apply appropriate security contexts
- Regularly update base images
- Implement proper logging and monitoring
- Use
.dockerignore
to exclude sensitive files - Lint Dockerfiles for best practices
- Remove unnecessary tools and packages
Command Reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Build with BuildKit (recommended)
DOCKER_BUILDKIT=1 docker build -t myapp:latest .
# Build specific stage
docker build --target build -t myapp-build .
# Build with build args
docker build --build-arg VERSION=1.2.3 -t myapp:1.2.3 .
# Build with secrets
DOCKER_BUILDKIT=1 docker build --secret id=api_key,src=./api_key.txt -t myapp .
# Scan image for vulnerabilities
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image myapp:latest
# Sign image
export DOCKER_CONTENT_TRUST=1
docker push myorg/myapp:latest
# Run with security options
docker run --security-opt=no-new-privileges --cap-drop=ALL --read-only myorg/myapp:latest
By following the guidance in this document, you’ll be able to create more secure, efficient, and maintainable Docker images using multi-stage builds. Remember that container security is a continuous process that requires regular updates, monitoring, and adherence to best practices.