Skip to content
Unverified — AI-generated content. Help verify this page

Docker Image Optimization

Why It Exists

Docker image size and build time directly impact every stage of the software delivery pipeline:

Impact AreaLarge Image ProblemOptimized Image Benefit
CI/CD build time10-15 minutes per build1-3 minutes per build
Registry storage$50-200/month for 100 images$5-20/month
Pull time (deploy)30-60 seconds2-5 seconds
Horizontal scalingNew pods take 60s to startNew pods in 5s
Security surface200+ CVEs in large images0-10 CVEs
Network egress$100+/month in transfer costs$10/month

At scale, these differences are multiplied. A company with 50 microservices deploying 10 times per day moves:

Daily transfer=50×10×3 replicas×Simage

With 500MB images: 50×10×3×500MB=750GB/day

With 50MB images: 50×10×3×50MB=75GB/day

The 10x reduction saves approximately $2,000/month in network costs alone, plus the immeasurable time savings for developers waiting for builds and deployments.

First Principles

What Makes Images Large

Every Docker image is a stack of layers. Each layer adds files. Understanding where the bytes come from is the first step to optimization.

The Four Optimization Levers

  1. Base image — The foundation. Alpine is 7MB, Debian is 125MB, Ubuntu is 77MB.
  2. Layer structure — What you install and how you clean up.
  3. Build context — What gets sent to the Docker daemon.
  4. Caching strategy — How to maximize cache hits for fast rebuilds.

Core Mechanics

Analyzing Images with Dive

Dive is an interactive tool that visualizes image layers and identifies wasted space.

bash
# Install dive
# macOS
brew install dive

# Linux
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.tar.gz
tar -xzf dive_0.12.0_linux_amd64.tar.gz
sudo mv dive /usr/local/bin/

# Analyze an image
dive myapp:latest

# CI mode (fails if efficiency is below threshold)
CI=true dive myapp:latest --ci-config .dive-ci.yaml

.dive-ci.yaml:

yaml
rules:
  # If the efficiency is below this percentage, the CI test will fail
  lowestEfficiency: 0.95

  # If the amount of wasted space is above this, the CI test will fail
  highestWastedBytes: 20000000  # 20MB

  # If the number of wasted files is above this, the CI test will fail
  highestUserWastedPercent: 0.10  # 10%

Dive output interpretation:

Layer Details:
  Digest: sha256:abc123...
  Size:   156 MB
  Command: RUN npm ci

  Added files:
    /app/node_modules/typescript/       45 MB  ← dev dependency in prod!
    /app/node_modules/@types/           23 MB  ← type definitions in prod!
    /app/node_modules/jest/             34 MB  ← test framework in prod!
    /app/node_modules/express/          12 MB  ← needed
    /root/.npm/_cacache/                42 MB  ← npm cache not cleaned!

Using docker history for quick analysis:

bash
# View layer sizes
docker history myapp:latest --format "table {​{.Size}}\t{​{.CreatedBy}}" --no-trunc

# Example output:
# SIZE      CREATED BY
# 0B        CMD ["node" "dist/server.js"]
# 5MB       COPY dist/ ./dist/
# 100MB     RUN npm ci --production
# 50KB      COPY package.json package-lock.json ./
# 0B        WORKDIR /app
# 180MB     Base image layers

.dockerignore Deep Dive

The .dockerignore file controls what gets sent as the build context. Without it, Docker sends everything in the build directory to the daemon.

bash
# Check build context size
docker build --no-cache . 2>&1 | head -5
# Sending build context to Docker daemon  523.8MB  ← Too large!

Comprehensive .dockerignore:

# Version control
.git
.gitignore
.gitattributes

# CI/CD
.github
.gitlab-ci.yml
.circleci
Jenkinsfile
.travis.yml

# Docker
Dockerfile*
docker-compose*
.dockerignore

# IDE and editor
.vscode
.idea
*.swp
*.swo
*~
.editorconfig

# Documentation
*.md
docs/
LICENSE
CHANGELOG*

# Testing
coverage/
.nyc_output/
__tests__/
test/
tests/
*.test.js
*.test.ts
*.spec.js
*.spec.ts
jest.config.*
.jest/

# Node.js
node_modules/
npm-debug.log*
.npm/

# Build output (rebuilt in container)
dist/
build/
out/
.next/

# Environment files
.env
.env.*
!.env.example

# OS files
.DS_Store
Thumbs.db
*.tmp

# Python
__pycache__/
*.py[cod]
*.egg-info/
.venv/
venv/
.tox/
.mypy_cache/

# Go
vendor/ (if using modules)

# Misc
*.log
*.bak
*.backup
tmp/
temp/

Impact measurement:

bash
# Before .dockerignore
du -sh .
# 523MB

# After .dockerignore
# Create a tar to simulate what Docker sends
tar -czf /dev/null -T <(git ls-files) 2>&1
# Or:
docker build --no-cache . 2>&1 | grep "Sending"
# Sending build context to Docker daemon  4.2MB  ← 125x smaller

Layer Optimization Techniques

1. Combine RUN Commands

Each RUN creates a layer. Combining related commands reduces layers and eliminates intermediate files:

dockerfile
# BAD: 3 layers, intermediate apt cache persists
RUN apt-get update
RUN apt-get install -y curl wget
RUN rm -rf /var/lib/apt/lists/*

# GOOD: 1 layer, apt cache cleaned in same layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    curl \
    wget && \
    rm -rf /var/lib/apt/lists/*

WARNING

Deleting files in a separate RUN command does NOT reduce image size. The files still exist in the previous layer. They must be deleted in the same RUN command where they were created.

dockerfile
# BAD: File still exists in Layer 1
RUN wget https://example.com/large-file.tar.gz && tar xzf large-file.tar.gz  # Layer 1: +500MB
RUN rm large-file.tar.gz                                                       # Layer 2: 0 size change

# GOOD: File downloaded, extracted, and removed in same layer
RUN wget https://example.com/large-file.tar.gz && \
    tar xzf large-file.tar.gz && \
    rm large-file.tar.gz  # Layer 1: only extracted files remain

2. Package Manager Optimization

Node.js:

dockerfile
# Install production deps only
RUN npm ci --production

# Clean npm cache
RUN npm ci --production && npm cache clean --force

# Remove unnecessary files from node_modules
RUN npm ci --production && \
    npm cache clean --force && \
    find node_modules -name "*.md" -delete && \
    find node_modules -name "*.txt" -delete && \
    find node_modules -name "LICENSE*" -delete && \
    find node_modules -name "CHANGELOG*" -delete && \
    find node_modules -name "*.map" -delete && \
    find node_modules -name "*.d.ts" -delete && \
    find node_modules -name ".package-lock.json" -delete

Python:

dockerfile
# Install without cache and compile
RUN pip install --no-cache-dir --no-compile -r requirements.txt

# Remove __pycache__ and .pyc files
RUN pip install --no-cache-dir -r requirements.txt && \
    find /usr/local/lib/python3.12 -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null; \
    find /usr/local/lib/python3.12 -name "*.pyc" -delete 2>/dev/null; \
    true

# Use --only-binary to avoid compiling from source
RUN pip install --no-cache-dir --only-binary=:all: -r requirements.txt

Alpine APK:

dockerfile
# Install and clean in one layer
RUN apk add --no-cache curl wget

# Use --virtual for build dependencies (easy cleanup)
RUN apk add --no-cache --virtual .build-deps \
    build-base \
    python3-dev && \
    npm ci && \
    apk del .build-deps

Debian APT:

dockerfile
# Clean apt cache and lists
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    libpq5 \
    curl && \
    rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*

3. Copy Optimization

dockerfile
# BAD: Copy everything, invalidating all subsequent cache
COPY . .

# GOOD: Copy in order of change frequency (least to most)
COPY package.json package-lock.json ./   # Changes: weekly
RUN npm ci --production
COPY tsconfig.json ./                     # Changes: monthly
COPY src/ ./src/                          # Changes: daily
RUN npm run build

4. Use Specific COPY Targets

dockerfile
# BAD: Copies everything including test files, docs, etc.
COPY . .

# GOOD: Copy only what's needed
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./

Base Image Selection Impact

Base ImageCompressed SizeUncompressedPackagesCVEs (typical)
scratch0000
alpine:3.193.4MB7.7MB~150-2
gcr.io/distroless/static1.2MB2.5MB~50
debian:12-slim30MB77MB~605-20
ubuntu:22.0429MB77MB~9010-40
node:20-alpine55MB180MB~252-10
node:20-slim70MB200MB~8010-30
node:20340MB1GB~40050-150
python:3.12-alpine18MB55MB~252-5
python:3.12-slim50MB140MB~8010-30
python:3.12340MB1GB~40050-150
golang:1.22-alpine100MB270MB~302-10
golang:1.22280MB820MB~20020-60

BuildKit Cache Mounts

Cache mounts persist package manager caches between builds, dramatically speeding up dependency installation:

dockerfile
# syntax=docker/dockerfile:1

# npm: cache the npm store
RUN --mount=type=cache,target=/root/.npm \
    npm ci --production

# pip: cache the pip downloads
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

# Go: cache modules and build cache
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /server ./cmd/server

# apt: cache the package lists and downloads
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt/lists \
    apt-get update && apt-get install -y libpq-dev

# Cargo: cache the registry and build artifacts
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release && \
    cp target/release/server /usr/local/bin/

Cache mount performance impact:

Package ManagerWithout Cache MountWith Cache MountSpeedup
npm ci (500 packages)45s12s3.75x
pip install (50 packages)30s8s3.75x
go build (large project)60s15s4x
cargo build180s30s6x
apt-get install20s5s4x

Implementation — Optimization Workflow

Step-by-Step Image Size Audit

typescript
import { execSync } from 'child_process';

interface LayerInfo {
  id: string;
  createdBy: string;
  size: number;
  sizeHuman: string;
}

interface OptimizationReport {
  imageName: string;
  totalSize: number;
  totalSizeHuman: string;
  layers: LayerInfo[];
  recommendations: string[];
}

function auditImage(imageName: string): OptimizationReport {
  // Get layer information
  const historyOutput = execSync(
    `docker history ${imageName} --format "{​{.ID}}|||{​{.CreatedBy}}|||{​{.Size}}" --no-trunc`,
    { encoding: 'utf-8' },
  );

  const layers: LayerInfo[] = historyOutput
    .trim()
    .split('\n')
    .map((line) => {
      const [id, createdBy, sizeStr] = line.split('|||');
      return {
        id: id.trim(),
        createdBy: createdBy.trim(),
        size: parseSize(sizeStr.trim()),
        sizeHuman: sizeStr.trim(),
      };
    })
    .filter((l) => l.size > 0);

  const totalSize = layers.reduce((sum, l) => sum + l.size, 0);
  const recommendations: string[] = [];

  // Check for common issues
  for (const layer of layers) {
    const cmd = layer.createdBy;

    // Check for npm install without --production
    if (cmd.includes('npm install') && !cmd.includes('--production') && !cmd.includes('npm ci')) {
      recommendations.push(
        `Layer "${cmd.slice(0, 80)}..." uses npm install without --production. ` +
        `This includes devDependencies. Use "npm ci --production" instead.`,
      );
    }

    // Check for apt without cleanup
    if (cmd.includes('apt-get install') && !cmd.includes('rm -rf /var/lib/apt')) {
      recommendations.push(
        'apt-get install without cleanup. Add "rm -rf /var/lib/apt/lists/*" ' +
        'in the same RUN command.',
      );
    }

    // Check for large layers
    if (layer.size > 100_000_000) {
      recommendations.push(
        `Large layer (${layer.sizeHuman}): "${cmd.slice(0, 100)}...". ` +
        'Consider splitting or optimizing this layer.',
      );
    }

    // Check for COPY of everything
    if (cmd.includes('COPY . .') || cmd.includes('COPY dir:.')) {
      recommendations.push(
        'COPY . . detected. Ensure .dockerignore excludes unnecessary files. ' +
        'Consider copying specific directories instead.',
      );
    }
  }

  // Check for multi-stage usage
  const inspectOutput = execSync(
    `docker inspect ${imageName} --format "{​{len .RootFS.Layers}}"`,
    { encoding: 'utf-8' },
  );
  const layerCount = parseInt(inspectOutput.trim(), 10);
  if (layerCount > 15) {
    recommendations.push(
      `Image has ${layerCount} layers. Consider reducing layers by combining ` +
      'RUN commands or using multi-stage builds.',
    );
  }

  // Check base image
  const baseImage = layers[layers.length - 1]?.createdBy ?? '';
  if (baseImage.includes('ubuntu') || baseImage.includes('debian:') && !baseImage.includes('slim')) {
    recommendations.push(
      'Using full Debian/Ubuntu base. Consider Alpine or distroless for smaller images.',
    );
  }

  return {
    imageName,
    totalSize,
    totalSizeHuman: formatSize(totalSize),
    layers: layers.sort((a, b) => b.size - a.size),
    recommendations,
  };
}

function parseSize(sizeStr: string): number {
  const match = sizeStr.match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i);
  if (!match) return 0;
  const value = parseFloat(match[1]);
  const unit = match[2].toUpperCase();
  const multipliers: Record<string, number> = {
    B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4,
  };
  return value * (multipliers[unit] ?? 0);
}

function formatSize(bytes: number): string {
  const units = ['B', 'KB', 'MB', 'GB'];
  let unitIndex = 0;
  let size = bytes;
  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex++;
  }
  return `${size.toFixed(1)} ${units[unitIndex]}`;
}

// Usage
const report = auditImage('myapp:latest');
console.log(`Image: ${report.imageName}`);
console.log(`Total size: ${report.totalSizeHuman}`);
console.log('\nLargest layers:');
for (const layer of report.layers.slice(0, 5)) {
  console.log(`  ${layer.sizeHuman}: ${layer.createdBy.slice(0, 80)}`);
}
console.log('\nRecommendations:');
for (const rec of report.recommendations) {
  console.log(`  - ${rec}`);
}

CI/CD Image Size Gate

yaml
# GitHub Actions workflow that fails if image is too large
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:test .

      - name: Check image size
        run: |
          MAX_SIZE_MB=150
          SIZE_BYTES=$(docker inspect myapp:test --format='{​{.Size}}')
          SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
          echo "Image size: ${SIZE_MB}MB (limit: ${MAX_SIZE_MB}MB)"
          if [ "$SIZE_MB" -gt "$MAX_SIZE_MB" ]; then
            echo "Image exceeds size limit!"
            docker history myapp:test --format "table {​{.Size}}\t{​{.CreatedBy}}" --no-trunc
            exit 1
          fi

      - name: Run dive CI
        uses: wagoodman/dive-action@master
        with:
          image: myapp:test
          config: .dive-ci.yaml

Edge Cases and Failure Modes

1. Multi-Stage COPY Creates New Layer

Even when using multi-stage builds, COPY --from creates a new layer with the full file content:

dockerfile
# Each COPY creates a layer
COPY --from=builder /app/dist ./dist           # Layer 1: 5MB
COPY --from=deps /app/node_modules ./node_modules # Layer 2: 80MB

# No way to avoid this — it's by design
# But you can minimize by only copying what's needed

2. Docker Cache Invalidation with Timestamps

If your build tool generates files with timestamps (e.g., Java .class files), the checksum changes even when content is identical, invalidating Docker's layer cache:

dockerfile
# Fix for Java: use reproducible builds
RUN mvn package -Dproject.build.outputTimestamp=2024-01-01T00:00:00Z

# Fix for Go: strip build info
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-w -s' -o /server

3. Platform-Specific Image Sizes

The same Dockerfile produces different sizes on different architectures:

Imageamd64arm64Difference
alpine:3.193.4MB3.3MB~3%
node:20-alpine55MB52MB~5%
golang:1.22280MB265MB~5%
Go binary12MB11MB~8%

4. Squashing Layers (When to Use)

Docker supports squashing all layers into one, which can reduce size by eliminating deleted files in intermediate layers:

bash
# Squash during build (experimental)
docker build --squash -t myapp:squashed .

# With BuildKit, use a single-layer export
docker buildx build --output type=docker -t myapp:squashed .

WARNING

Squashing eliminates all layer sharing. If 10 services share a common base layer, squashing each means the base layer is stored 10 times instead of once. Only squash when layer sharing is not applicable.

Docker follows symlinks when building the context. A symlink to a large directory outside the build context can inflate the context size:

bash
# Check for symlinks in the build context
find . -type l -ls

# Ensure .dockerignore catches symlinked directories

Performance Characteristics

Build Time by Optimization Level

OptimizationClean BuildCode ChangeDep Change
No optimization120s120s120s
Layer ordering120s15s90s
+ .dockerignore110s10s85s
+ Cache mounts90s10s30s
+ Multi-stage100s10s30s
+ BuildKit parallel70s8s25s
+ Registry cache70s (cold)8s25s

Image Pull Time vs Size

Tpull=Tauth+ScompressedBW+TextractTextractSuncompressed500MB/s+Nlayers×100ms
Image Size (compressed)100Mbps1Gbps10Gbps
5MB (Go scratch)0.4s0.1s0.05s
50MB (Alpine)4.0s0.4s0.1s
200MB (Slim)16s1.6s0.2s
500MB (Full OS)40s4.0s0.4s
1GB (Unoptimized)80s8.0s0.8s

Compression Ratios

Docker uses gzip for layer compression. Different content types compress differently:

Rcompression=1ScompressedSuncompressed
Content TypeCompression RatioExample
JavaScript source70-80%100KB becomes 20-30KB
Compiled Go binary55-65%30MB becomes 10-13MB
Python bytecode40-50%50MB becomes 25-30MB
Node modules (mixed)65-75%200MB becomes 50-70MB
SQLite databases30-40%100MB becomes 60-70MB
Docker base OS layers50-65%77MB becomes 27-38MB

Registry Storage Cost Analysis

Monthly cost=Nimages×Rretention×Savg×Cper_GB

For 50 microservices, 30 retained tags each:

ScenarioAvg ImageTotal StorageCost (at $0.10/GB)
Unoptimized500MB750GB$75/month
Optimized50MB75GB$7.50/month
With dedup50MB (15MB unique)22.5GB$2.25/month

Mathematical Foundations

Optimal Layer Ordering Proof

Given operations o1,...,on with change probabilities p1,...,pn and execution costs c1,...,cn, the expected rebuild cost is:

E[C]=i=1npij=incj

Theorem: This is minimized when operations are sorted by decreasing cipi (cost-to-frequency ratio).

Proof: Consider swapping adjacent operations oi and oi+1. The cost difference:

ΔE=pici+1pi+1ci

The swap is beneficial when ΔE<0:

pici+1<pi+1cicipi>ci+1pi+1

So operations with higher cp ratio should come first. This gives the optimal ordering:

OperationCostFrequencyc/pOrder
Install OS packages20s1/month6001st
Install dependencies60s2/week2102nd
Copy config files1s1/week73rd
Copy source code3s5/day0.44th
Run build15s5/day2.13rd-4th

Image Deduplication Analysis

For N images sharing k common layers with total common size Sc and unique sizes S1,...,SN:

Sdeduplicated=Sc+i=1NSiSnaive=NSc+i=1NSiSavings ratio=(N1)ScNSc+Si

This approaches N1N as common layers dominate, meaning:

  • 2 images sharing layers: up to 50% savings
  • 10 images: up to 90% savings
  • 100 images: up to 99% savings

Real-World War Stories

War Story — The 4GB Docker Image

A machine learning team built a Docker image that included the full CUDA toolkit (3.5GB), PyTorch (~2GB installed), their model weights (500MB), and the training data (1GB). Total: 7GB. Pulling this image took 5 minutes even on AWS's internal network.

Optimization path:

  1. Used NVIDIA's runtime base instead of full CUDA toolkit: -2.5GB
  2. Moved model weights to S3, downloaded in init container: -500MB
  3. Removed training data (not needed at inference): -1GB
  4. Used multi-stage build to exclude build tools: -500MB
  5. Final image: 2.5GB (64% reduction)

They further reduced deployment time by using an Amazon ECR pull-through cache in each region.

War Story — The npm Cache That Ate Production

A team noticed their production containers were using 200MB more disk than expected. Investigation with dive revealed that npm ci was writing to /root/.npm/_cacache/ (the npm cache), which persisted in the image layer. Over multiple dependency updates, old cached packages accumulated.

Fix:

dockerfile
RUN npm ci --production && npm cache clean --force

Or better, with a cache mount that does not persist in the image:

dockerfile
RUN --mount=type=cache,target=/root/.npm npm ci --production

War Story — The .git Directory in Production

A company's container scanner flagged their production image for containing Git credentials. Investigation revealed that COPY . . had included the .git/ directory, which contained the .git/config file with a GitHub personal access token in the remote URL. The .git/ directory also added 400MB to the image (full Git history).

Fix: Added .git to .dockerignore and rotated the exposed token. Now enforce .dockerignore review as part of the PR process.

Decision Framework

When to Optimize

SituationEffortImpactPriority
Image > 500MBLow (add .dockerignore, multi-stage)HighImmediate
Build takes > 5minMedium (layer reordering, cache mounts)HighThis sprint
10+ microservicesMedium (shared base, registry cache)HighThis quarter
< 50MB, < 1min buildN/ALowDon't optimize

Optimization Checklist

  • [ ] .dockerignore excludes .git, node_modules, dist, docs, tests
  • [ ] Multi-stage build separates build from runtime
  • [ ] Dependencies copied before source code (cache optimization)
  • [ ] Alpine or distroless base image
  • [ ] npm ci --production or equivalent
  • [ ] Package manager cache cleaned in same RUN
  • [ ] No unnecessary files in final image
  • [ ] BuildKit cache mounts for package managers
  • [ ] CI gate for image size
  • [ ] dive audit passes with >95% efficiency

Advanced Topics

Nix-Based Docker Builds

Nix provides perfectly reproducible builds with minimal image sizes:

nix
# flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11";
  };

  outputs = { self, nixpkgs }:
    let
      pkgs = nixpkgs.legacyPackages.x86_64-linux;
    in {
      packages.x86_64-linux.docker = pkgs.dockerTools.buildLayeredImage {
        name = "myapp";
        tag = "latest";
        contents = [
          pkgs.nodejs_20
          ./dist  # Application code
        ];
        config = {
          Cmd = [ "${pkgs.nodejs_20}/bin/node" "/dist/server.js" ];
          ExposedPorts = { "3000/tcp" = {}; };
        };
      };
    };
}

Slim (DockerSlim) for Automatic Optimization

bash
# Automatically minify an image by analyzing what files are actually used
docker-slim build --target myapp:latest --http-probe-cmd /health

# Typical results:
# Original: 350MB
# Slimmed: 35MB (90% reduction)

# WARNING: Slim removes files not accessed during probing
# Ensure all code paths are exercised during the probe

Layer-Level Compression with zstd

Docker now supports zstd compression (better ratio and speed than gzip):

bash
# Build with zstd compression (BuildKit)
docker buildx build \
  --output type=image,compression=zstd,compression-level=3 \
  -t myapp:latest .
CompressionRatioCompress SpeedDecompress Speed
gzip (default)~65%50 MB/s400 MB/s
zstd (level 3)~65%300 MB/s800 MB/s
zstd (level 19)~70%15 MB/s800 MB/s

zstd level 3 provides similar compression to gzip with 6x faster compression and 2x faster decompression.

Lazy Image Loading (eStargz/Nydus)

Instead of pulling the entire image before starting, lazy loading streams layers on demand:

bash
# Convert image to eStargz format
ctr-remote image optimize myapp:latest myapp:estargz

# Stargz snapshotter downloads only needed files on first access
# Cold start: 300ms vs 30s for a 500MB image
Tstartlazy=Tmetadata+Tentrypoint_filesTfull_pull

This is particularly impactful for large images (ML models, Java applications) where only a fraction of the image is needed at startup.


Previous: Compose Patterns | Back to Docker Overview.

"What I cannot create, I do not understand." — Richard Feynman