20.2.2.4 Image Size Practice
A focused guide to Image Size Practice, connecting core concepts with practical Docker and container operations.
Image size practice is the hands-on work of measuring, diagnosing, and reducing Docker image sizes using the techniques available in the Docker toolchain. Knowing that layer caching, multi-stage builds, and minimal base images reduce size is different from applying them systematically to a real image and verifying the results. This practice provides the specific commands and diagnostic steps to analyze what is in an image and where its size comes from.
Measuring Image Size
The starting point for any optimization effort is knowing the current size:
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
my-api unoptimized a1b2c3d4e5f6 2 minutes ago 1.14GB
my-api optimized b2c3d4e5f6a1 1 minute ago 98MB
To list all images sorted by size:
docker images --format "{{.Repository}}:{{.Tag}}\t{{.Size}}" | sort -k2 -rh
Inspecting Layer Sizes
docker history shows the size contribution of each layer in the image:
docker history my-api:unoptimized
IMAGE CREATED BY SIZE
a1b2c3d4e5f6 /bin/sh -c #(nop) CMD ["node" "dist/server.js"] 0B
b2c3d4e5f6a1 /bin/sh -c #(nop) EXPOSE 3000 0B
c3d4e5f6a1b2 /bin/sh -c #(nop) COPY dir:... in /app 5.2MB
d3e4f5a6b7c8 /bin/sh -c npm install 320MB
e4f5a6b7c8d9 /bin/sh -c #(nop) COPY file:... in /app 2.1KB
f5a6b7c8d9e0 /bin/sh -c #(nop) WORKDIR /app 0B
a6b7c8d9e0f1 /bin/sh -c #(nop) FROM node:20 1.1GB
Reading from the bottom: the base image contributes 1.1GB, npm install adds 320MB, and source files add 5.2MB. The total is dominated by the full Node.js base image and the complete dev dependency tree.
For the optimized image:
docker history my-api:optimized
IMAGE CREATED BY SIZE
... /bin/sh -c #(nop) CMD ... 0B
... /bin/sh -c #(nop) COPY ... 45.2MB
... /bin/sh -c npm ci --only=production 52.1MB
... /bin/sh -c #(nop) WORKDIR /app 0B
... /bin/sh -c #(nop) FROM node:20-alpine 135MB
The base image is now 135MB instead of 1.1GB. The npm ci --only=production layer installs only production dependencies. The total is 232MB before compression.
Examining What Is Inside an Image
To explore the contents of an image without running a full container, run an ephemeral shell:
docker run --rm -it my-api:unoptimized sh
Inside the container, identify large directories:
du -sh /app/* | sort -rh | head -20
315M /app/node_modules
5.2M /app/dist
2.1K /app/package.json
The node_modules directory at 315MB is the largest contributor. In the optimized image, only production dependencies are present:
docker run --rm -it my-api:optimized sh
du -sh /app/node_modules
48M /app/node_modules
An 84% reduction in node_modules size by excluding dev dependencies.
Identifying Unnecessarily Included Files
Check whether build artifacts that should have been excluded by .dockerignore ended up in the image:
docker run --rm my-api:unoptimized ls /app
Look for directories that should not be present:
__tests__ortest/— test files have no place in a production image.git— version control history is never needed at runtimenode_modulesfrom the host — if the host'snode_moduleswas copied instead of installed inside the container
docker run --rm my-api:unoptimized ls /app/node_modules | wc -l
Counts the number of packages in node_modules. Compare against a build with npm ci --only=production to verify dev dependencies are absent.
Checking Whether Package Manager Caches Were Cleaned
For images using apt-get:
docker run --rm my-image:latest ls /var/lib/apt/lists/
If this directory is non-empty, the package manager cache was not cleaned in the same RUN layer that installed packages. The cache is consuming image space without providing any runtime value.
Correct approach:
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
For pip:
docker run --rm my-python-image:latest ls ~/.cache/pip
Use --no-cache-dir to prevent pip from caching:
RUN pip install --no-cache-dir -r requirements.txt
Side-by-Side Comparison Workflow
Build two versions of the same application: the naive version and the optimized version:
docker build -f Dockerfile.naive -t my-api:naive .
docker build -f Dockerfile.optimized -t my-api:optimized .
docker images | grep my-api
my-api naive a1b2c3d4 1.14GB
my-api optimized b2c3d4e5 98MB
Document the size reduction after each optimization step:
- Baseline (full node:20 + all deps + source): 1.14GB
- After switching to
node:20-alpine: 320MB (72% reduction) - After adding
.dockerignore: 290MB (skippingnode_modulesfrom host) - After
--only=production: 175MB - After multi-stage build: 98MB (91% total reduction)
Using Docker Scout for Size and CVE Analysis
Docker Scout provides a detailed breakdown of what is in an image:
docker scout cves my-api:unoptimized
docker scout cves my-api:optimized
The CVE count typically drops significantly with the optimized image because fewer packages are present.
docker scout recommendations my-api:unoptimized
Scout suggests alternative base images or configuration changes that would reduce size or CVE count, including estimated size changes for each recommendation.
Practical Size Targets
| Application type | Realistic optimized size |
|---|---|
| Node.js (Alpine + prod deps + compiled) | 80–200MB |
| Python (slim + pip install) | 100–250MB |
| Go (scratch + static binary) | 5–30MB |
| Java (JRE Alpine + JAR) | 150–300MB |
| Static file server (nginx:alpine + files) | 20–50MB |
These ranges account for production dependency trees. Images that exceed these targets by 2x or more typically have an optimization opportunity in the base image choice, dependency exclusion, or build artifact cleanup.