Introduction
Optimizing Docker images is crucial for production deployments. Smaller, more efficient images lead to faster deployments, reduced attack surface, and lower costs. This guide covers advanced techniques for building production-ready Docker images.
Multi-Stage Builds
.NET Application Example
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /source
# Copy csproj and restore dependencies (this is done separately to leverage Docker layer caching)
COPY *.csproj .
RUN dotnet restore
# Copy everything else and build
COPY . .
RUN dotnet publish -c Release -o /app --no-restore
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime
WORKDIR /app
# Install required packages and create non-root user
RUN apk add --no-cache \
icu-libs \
tzdata \
&& adduser -D -u 1000 appuser
# Copy published app from build stage
COPY --from=build --chown=appuser:appuser /app .
# Switch to non-root user
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet"
- MyApp.dll
Node.js Application with Dependencies Optimization
# Dependencies stage
FROM node:20-alpine AS deps
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install production dependencies
RUN npm ci --only=production
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files and install all dependencies
COPY package*.json ./
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Runtime stage
FROM node:20-alpine AS runtime
WORKDIR /app
# Install dumb-init for proper signal handling
RUN apk add --no-cache dumb-init
# Create non-root user
RUN addgroup -g 1001 nodejs && \
adduser -S -u 1001 -G nodejs nodejs
# Copy production dependencies
COPY --from=deps --chown=nodejs:nodejs /app/node_modules ./node_modules
# Copy built application
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --chown=nodejs:nodejs package*.json ./
# Switch to non-root user
USER nodejs
# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init"
- --
CMD ["node"
- dist/index.js
Layer Caching Strategies
Optimizing Layer Order
# Bad: Changes to code invalidate dependency cache
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python"
- app.py
# Good: Dependencies cached separately
FROM python:3.11-slim
WORKDIR /app
# Install dependencies first (cached if requirements.txt doesn't change)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code (changes don't invalidate dependency cache)
COPY . .
CMD ["python"
- app.py
Using BuildKit Cache Mounts
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Cache go modules
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
# Copy source code
COPY . .
# Build with cache mount for go build cache
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=linux go build -o app .
# Runtime stage
FROM scratch
COPY --from=builder /app/app /app
ENTRYPOINT ["/app
Security Best Practices
Distroless Images
# Build stage
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# Runtime stage using distroless
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server
Security Scanning in Build
# Build stage with security scanning
FROM node:20-alpine AS builder
WORKDIR /app
# Install security scanning tools
RUN apk add --no-cache npm
RUN npm install -g npm-audit-resolver
COPY package*.json ./
RUN npm ci && npm audit --production
COPY . .
RUN npm run build
# Scan for vulnerabilities with Trivy
FROM aquasec/trivy:latest AS scanner
COPY --from=builder /app /app
RUN trivy fs --no-progress --security-checks vuln,config /app
# Runtime stage
FROM node:20-alpine
WORKDIR /app
# Security hardening
RUN apk upgrade --no-cache && \
apk add --no-cache libcap && \
rm -rf /var/cache/apk/*
# Create non-root user with specific UID/GID
RUN addgroup -g 10001 appgroup && \
adduser -D -u 10001 -G appgroup appuser
# Copy application
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
# Drop all capabilities
RUN setcap -r /usr/local/bin/node || true
USER appuser
# Read-only root filesystem
# VOLUME ["/tmp
CMD ["node"
- dist/index.js
Image Size Optimization
Alpine vs Debian Slim Comparison
# Alpine-based image (smaller)
FROM python:3.11-alpine
RUN apk add --no-cache \
gcc \
musl-dev \
postgresql-dev
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Final size: ~150MB
# Debian-slim based image (more compatible)
FROM python:3.11-slim
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Final size: ~200MB
Minimizing Final Image Size
# Multi-stage build for Go application
FROM golang:1.21 AS builder
WORKDIR /build
# Enable Go modules
ENV GO111MODULE=on
ENV CGO_ENABLED=0
# Cache dependencies
COPY go.mod go.sum ./
RUN go mod download
# Build static binary
COPY . .
RUN go build \
-ldflags="-w -s -extldflags '-static'" \
-a -installsuffix cgo \
-o app .
# Compress binary with UPX
FROM alpine AS compressor
RUN apk add --no-cache upx
COPY --from=builder /build/app /app
RUN upx --best --lzma /app
# Final scratch image
FROM scratch
COPY --from=compressor /app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app
# Final size: ~5-10MB
Build Arguments and Secrets
Secure Secrets Handling
# syntax=docker/dockerfile:1
FROM node:20-alpine
# Use BuildKit secrets (not stored in image)
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN && \
npm install --production && \
npm config delete //registry.npmjs.org/:_authToken
# Build with: docker build --secret id=npm_token,src=.npmtoken .
Build Arguments for Versioning
ARG BUILD_DATE
ARG VCS_REF
ARG VERSION
FROM alpine:3.18
# Use build arguments for labels
LABEL org.opencontainers.image.created=$BUILD_DATE \
org.opencontainers.image.revision=$VCS_REF \
org.opencontainers.image.version=$VERSION \
org.opencontainers.image.title="My Application" \
org.opencontainers.image.description="Production-ready application"
# Runtime configuration
ENV APP_VERSION=$VERSION
Docker Compose for Development
Development vs Production Configs
# docker-compose.yml (base)
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
target: runtime
environment:
- NODE_ENV=production
ports:
- "3000:3000"
networks:
- app-network
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
redis-data:
# docker-compose.override.yml (development)
version: '3.8'
services:
app:
build:
target: development
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DEBUG=*
command: npm run dev
redis:
ports:
- "6379:6379"
Container Health and Lifecycle
Comprehensive Health Checks
FROM node:20-alpine
WORKDIR /app
COPY . .
# Install curl for health checks
RUN apk add --no-cache curl
# Health check script
COPY <<EOF /healthcheck.sh
#!/bin/sh
set -e
# Check if the application is responding
curl -f http://localhost:3000/health || exit 1
# Check if critical dependencies are accessible
curl -f http://localhost:3000/ready || exit 1
# Check memory usage
MEM_USAGE=$(cat /proc/1/status | grep VmRSS | awk '{print $2}')
MEM_LIMIT=524288 # 512MB in KB
if [ "$MEM_USAGE" -gt "$MEM_LIMIT" ]; then
exit 1
fi
exit 0
EOF
RUN chmod +x /healthcheck.sh
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD /healthcheck.sh
CMD ["node"
- server.js
Graceful Shutdown
FROM node:20-alpine
# Install tini for proper signal handling
RUN apk add --no-cache tini
WORKDIR /app
COPY . .
# Create shutdown script
COPY <<'EOF' /app/shutdown.sh
#!/bin/sh
echo "Received shutdown signal, gracefully stopping..."
kill -TERM $(pgrep node)
sleep 10
exit 0
EOF
RUN chmod +x /app/shutdown.sh
# Use tini as init system
ENTRYPOINT ["/sbin/tini"
- --
# Handle signals properly
STOPSIGNAL SIGTERM
CMD ["node"
- server.js
CI/CD Integration
GitHub Actions Docker Build
name: Docker Build and Push
on:
push:
branches: [main]
tags:
- 'v*']
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@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: myapp/api
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=myapp/api:buildcache
cache-to: type=registry,ref=myapp/api:buildcache,mode=max
build-args: |
BUILD_DATE=${{ github.event.head_commit.timestamp }}
VCS_REF=${{ github.sha }}
VERSION=${{ steps.meta.outputs.version }}
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp/api:${{ steps.meta.outputs.version }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Debugging and Troubleshooting
Debug Image
# Production image
FROM alpine:3.18 AS production
COPY --from=builder /app/binary /app/binary
USER nobody
ENTRYPOINT ["/app/binary
# Debug image with tools
FROM production AS debug
USER root
RUN apk add --no-cache \
curl \
wget \
netcat-openbsd \
bind-tools \
busybox-extras \
strace \
tcpdump
USER nobody
# Build debug image with: docker build --target debug -t myapp:debug .
Conclusion
Optimizing Docker images requires balancing size, security, and functionality. By implementing multi-stage builds, leveraging layer caching, following security best practices, and using appropriate base images, you can create efficient, secure, and maintainable Docker images for production use.
Comments