Skip to content
Go back

Optimizing Docker Build Performance and Image Size

Optimizing Docker Build Performance and Image Size

Introduction

Efficient Docker builds improve development velocity and reduce deployment costs. This guide covers multi-stage builds, layer caching, and image optimization techniques.

Prerequisites

Step 1: Multi-Stage Build Example

Create optimized Dockerfile:

# Multi-stage build for Node.js application
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./

# Dependencies stage
FROM base AS deps
RUN npm ci --only=production --frozen-lockfile

# Build stage
FROM base AS build
RUN npm ci --frozen-lockfile
COPY . .
RUN npm run build

# Runtime stage
FROM node:18-alpine AS runtime
WORKDIR /app

# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy only necessary files
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/public ./public

# Set ownership
RUN chown -R nextjs:nodejs /app
USER nextjs

EXPOSE 3000
CMD ["node", "dist/server.js"]

Step 2: Optimize .dockerignore

Create comprehensive .dockerignore:

# Version control
.git
.gitignore

# Dependencies
node_modules
npm-debug.log*

# Build outputs
dist
build
.next

# Development files
.env.local
.env.development
docker-compose.yml
docker-compose.override.yml

# Documentation
README.md
docs/

# IDE files
.vscode/
.idea/
*.swp
*.swo

# OS generated files
.DS_Store
Thumbs.db

# Test files
coverage/
.nyc_output
test/
*.test.js
*.spec.js

# Logs
*.log
logs/

Step 3: Layer Caching Strategy

Optimize layer ordering for maximum cache reuse:

FROM node:18-alpine AS base

# Install system dependencies (rarely changes)
RUN apk add --no-cache \
    dumb-init \
    && rm -rf /var/cache/apk/*

WORKDIR /app

# Copy package files first (changes less frequently)
COPY package*.json ./
COPY yarn.lock ./

# Install dependencies (cached if package.json unchanged)
RUN npm ci --only=production --frozen-lockfile

# Copy source code last (changes most frequently)
COPY . .

# Build application
RUN npm run build

# Runtime optimizations
ENV NODE_ENV=production
USER node

ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Step 4: BuildKit Features

Enable BuildKit for advanced features:

export DOCKER_BUILDKIT=1

Use cache mounts and bind mounts:

# syntax=docker/dockerfile:1
FROM node:18-alpine AS deps

WORKDIR /app
COPY package*.json ./

# Use cache mount for npm cache
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production --frozen-lockfile

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./

# Cache mount for build dependencies
RUN --mount=type=cache,target=/root/.npm \
    npm ci --frozen-lockfile

COPY . .
RUN npm run build

FROM node:18-alpine AS runtime
WORKDIR /app

# Copy from previous stages
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist

CMD ["node", "dist/server.js"]

Step 5: Distroless Images

Use minimal base images:

# Build stage
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --frozen-lockfile
COPY . .
RUN npm run build

# Production stage with distroless
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app

COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY --from=build /app/package.json ./

EXPOSE 3000
CMD ["dist/server.js"]

Step 6: Security Optimization

Secure Dockerfile practices:

FROM node:18-alpine AS base

# Security updates
RUN apk update && apk upgrade && \
    apk add --no-cache dumb-init && \
    rm -rf /var/cache/apk/*

# Create app directory and user
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

FROM base AS deps
COPY package*.json ./
USER nextjs

# Install with security audit
RUN npm ci --only=production --frozen-lockfile && \
    npm audit --audit-level moderate

FROM base AS build
COPY package*.json ./
RUN npm ci --frozen-lockfile
COPY --chown=nextjs:nodejs . .
USER nextjs
RUN npm run build

FROM base AS runtime
COPY --from=deps --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=nextjs:nodejs /app/dist ./dist

# Security: Remove package managers and add healthcheck
RUN npm uninstall -g npm && \
    rm -rf /usr/local/lib/node_modules/npm

USER nextjs
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD node healthcheck.js

EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]

Step 7: Build Performance Tools

Use build-time optimization tools:

#!/bin/bash
set -e

# Enable experimental features
export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

# Build with cache from registry
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --cache-from type=registry,ref=myapp:cache \
  --cache-to type=registry,ref=myapp:cache,mode=max \
  --push \
  -t myapp:latest .

# Analyze image size
docker images myapp:latest --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# Security scan
docker scout cves myapp:latest

Step 8: Image Analysis and Monitoring

Create image analysis script:

#!/bin/bash

IMAGE_NAME=${1:-myapp:latest}

echo "=== Image Analysis for $IMAGE_NAME ==="

# Basic image info
echo "Image size:"
docker images $IMAGE_NAME --format "table {{.Size}}"

# Layer analysis
echo -e "\nLayer sizes:"
docker history $IMAGE_NAME --format "table {{.CreatedBy}}\t{{.Size}}" | head -10

# Security vulnerabilities
echo -e "\nSecurity scan:"
docker scout cves $IMAGE_NAME --only-severity critical,high

# Resource usage test
echo -e "\nStarting container for resource test..."
CONTAINER_ID=$(docker run -d --memory=256m --cpus=0.5 $IMAGE_NAME)

sleep 5

echo "Container stats:"
docker stats $CONTAINER_ID --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}"

# Cleanup
docker stop $CONTAINER_ID
docker rm $CONTAINER_ID

Step 9: CI/CD Integration

GitHub Actions workflow for optimized builds:

name: Optimized Docker Build

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        
      - name: Login to Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          
      - name: Image Analysis
        run: |
          docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
            wagoodman/dive:latest ghcr.io/${{ github.repository }}:latest \
            --ci

Summary

Docker build optimization involves multi-stage builds, layer caching, minimal base images, and security practices. Use BuildKit features, analyze image sizes, and implement CI/CD pipelines for consistent, efficient builds.


Share this post on:

Previous Post
Deploying Node.js Apps to Kubernetes with Helm Charts
Next Post
Implementing GraphQL Federation with Apollo Server