DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Automating Unix Security Across Hybrid Clouds
  • Docker Hardened Images for Container Security
  • Advanced Docker Security: From Supply Chain Transparency to Network Defense
  • How Migrating to Hardened Container Images Strengthens the Secure Software Development Lifecycle

Trending

  • Optimizing High-Volume REST APIs Using Redis Caching and Spring Boot (With Load Testing Code)
  • A System Cannot Protect What It Does Not Understand
  • Building a RAG-Powered Bug Triage Agent With AWS Bedrock and OpenSearch k-NN
  • How SaaS Architectures Break at Scale — and the Engineering Decisions That Prevent It
  1. DZone
  2. Software Design and Architecture
  3. Containers
  4. Top 5 Tips to Shrink and Secure Docker Images

Top 5 Tips to Shrink and Secure Docker Images

You can shrink Docker images from over 1 GB to under 10 MB by using various techniques. The article talks about five tips that will help achieve this.

By 
Akash Lomas user avatar
Akash Lomas
·
Dec. 09, 25 · Analysis
Likes (4)
Comment
Save
Tweet
Share
1.7K Views

Join the DZone community and get the full member experience.

Join For Free

I used to settle for Docker images that were massive, sometimes in GBs. I realized that every megabyte matters, impacting everything from deployment speed and cloud costs to security. With time, I realize there are well-known best practices and advanced techniques to achieve the ultimate goal: a tiny, hardened 10 MB image.

Here’s my comprehensive guide on how I achieve this using minimal base images, mastering layers, and implementing strong security protocols.

1. Minimal Base Images

Your first step should always be to pick a leaner base image(s), which means moving away from bulky defaults like node:latest (often over 1 GB). I think of this as choosing a race car chassis instead of a cargo truck.

Choosing the Right Base for Speed

  1. Alpine as the starting point: If you opt for the Alpine variant (e.g., node:20.10.0-alpine), this immediately cuts the size to about 250 MB. Alpine is purpose-built for containers, stripping out everything but the essentials needed to run the application.
  2. The ultimate finish line — scratch and distroless: To get closer to that 10 MB target, use multi-stage builds (more on that later) and select the smallest possible base for the final production stage.
  • Scratch: If your application is a statically compiled binary (like Go), use FROM scratch. It's an empty base, no OS, no shell. It copies only the binary into it.
  • Distroless: If your application needs a runtime (like Java or Node) or core C libraries, use a Distroless image. These contain only the necessary runtime files, eliminating the package manager and shell, which are huge security and size liabilities.

Example code (Final Stage using Scratch):

Plain Text
 
# Stage 2: The Final Image (Absolute minimum base)
FROM scratch

# Copy the compiled binary from the 'builder' stage
COPY --from=builder /app/my_app /usr/bin/my_app
ENTRYPOINT ["/usr/bin/my_app"]


2. Layer Mastery: Speed and Space Efficiency

Every instruction in your Dockerfile creates a layer, and Docker’s ability to cache these layers is crucial for fast build times.

Optimizing Caching Order

You should structure your Dockerfile to maximize cache hits. The rule is simple: put the most stable layers at the top and the most frequently changing layers at the bottom.

  1. Copy dependencies first: Dependencies change less often than your source code. You should copy only the manifest file (package.json, requirements.txt) first, install dependencies, and only then copy the full source code. This way, if only your code changes, the long dependency install step is skipped.
  2. Use .dockerignore: Before any file is copied, ensure to use a comprehensive .dockerignore. This prevents unnecessary files like node_modules, .git folders, and .env files from ever entering the build context, saving time and preventing security leaks.

Example code (layer caching strategy):

Plain Text
 
# 1. Stable layer: Copy just the manifest
COPY package.json package-lock.json ./

# 2. Stable layer: Install dependencies (can be cached longer)
RUN npm install

# 3. Frequently changing layer: Copy all source code
COPY . .


Squashing Layers and Cleaning Up

Ironically, deleting a file in a new layer doesn’t shrink the image; it just adds a “deletion marker” because the previous layers are immutable.

To truly save space, you should combine installation and cleanup into a single RUN command using the && operator.

Example code (installation and cleanup in one layer):

Plain Text
 
# Single RUN command ensures the cache is cleaned before the layer 
# is finalized.
RUN apt-get update \
    && apt-get install -y --no-install-recommends my-package \
    # The essential cleanup steps:
    && apt-get autoremove -y \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*


3. The Final Cut: Multi-Stage Builds

This, in my opinion, is the most powerful technique. Multi-stage builds are the only way to genuinely discard everything used for building (compilers, dependencies, intermediate files) that isn’t needed for running.

  1. The builder stage: This stage uses a larger base (like node:alpine) to fetch dependencies, compile, and create the production artifacts (e.g., static HTML files).
  2. The final stage: This stage uses a tiny, minimal base (like nginx:alpine or scratch) and only copies the final, lean artifacts from the builder stage using the --from=builder directive. Everything else, like Node.js, NPM, and the build-time dependencies, is thrown away.

Example code (multi-stage build):

Plain Text
 
# --- Stage 1: Builder ---
FROM node:20.10.0-slim AS builder
WORKDIR /app

COPY package.json .
RUN npm install

COPY . .
RUN npm run build # Creates 'dist' folder

# --- Stage 2: Production (Tiny image) ---
FROM nginx:alpine

# Only copy the final, optimized build output from the builder stage

COPY --from=builder /app/dist /usr/share/nginx/html
# Image size is now tiny (e.g., 57MB)


4. Hardening the Image: Security Is Size

A smaller image is inherently more secure because it has less surface area for attack. You should always implement these two crucial steps to lock down your containers:

Run as a Non-Root User

Running processes as root is a critical vulnerability. If an attacker compromises the container, they gain root access within that environment. You should always create and switch to a dedicated, unprivileged user.

Plain Text
 
# Install dependencies as root (if needed)
FROM node:20.10.0-slim

# Create a non-root user
RUN adduser --disabled-password --gecos "" appuser

# Switch to the non-root user for all runtime commands
USER appuser


No Secrets in Layers: BuildKit

You should never use ARG or ENV for secrets during the build, as they are recorded in the image history. Instead, use Docker's BuildKit secrets feature to temporarily mount a secret file during the specific RUN command that needs it, ensuring the secret never touches a persistent layer.

Plain Text
 
# Build with: docker build --secret id=npmkey,src=~/.npmrc .

# The secret is only accessible during this RUN command, then it disappears.
RUN --mount=type=secret,id=npmkey npm install


5. Optimization Tools

I rely on these tools to verify my process and find hidden bloat:

  • Dive: I use this image explorer to visually inspect every layer. It quickly highlights where files were added and, more importantly, where size was not saved (i.e., when I forgot to clean a cache).
  • Slim.AI: For the final optimization push, I use Slim to automatically analyze my application at runtime and build a truly minimal version. It often helps me eliminate those last few unnecessary libraries and tools that get me down to the 10 MB final size.

Hope these five tips will help you in reducing image size and securing your containers as well.

Cache (computing) Docker (software) security

Opinions expressed by DZone contributors are their own.

Related

  • Automating Unix Security Across Hybrid Clouds
  • Docker Hardened Images for Container Security
  • Advanced Docker Security: From Supply Chain Transparency to Network Defense
  • How Migrating to Hardened Container Images Strengthens the Secure Software Development Lifecycle

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook