codeWithYoha logo
Code with Yoha
Docker

Reduce Docker Image Size from 1GB to 150MB: Methods to Optimize Docker Images

Reduce Docker Image Size from 1GB to 150MB: Methods to Optimize Docker Images
6 min read
#Docker

Reducing Docker image size is essential for efficient application deployment and resource management. This guide will focus on optimizing Docker images—particularly for Java applications using JDK images—and provide practical steps to cut down an image from around 1GB to a target of 150MB. Alongside this, we’ll cover general Docker optimization techniques applicable to any image.

Why Optimize Docker Images?

Large Docker images come with various drawbacks, such as:

  • Increased Build Times: Large images take longer to build, slowing down development workflows.
  • Slower Deployment: Pushing and pulling large images can slow down CI/CD pipelines.
  • Higher Storage Costs: Images stored in registries or deployed in production environments consume more storage resources.

For Java applications, Docker images can grow due to the inclusion of the JDK, dependencies, build files, and other artifacts. Optimizing these images helps reduce resource usage and speeds up deployment.

Key Strategies to Optimize Docker Images

These strategies will help reduce the size of any Docker image and are especially relevant for JDK-based images:

1. Select a Minimal Base Image

One of the easiest ways to reduce Docker image size is by starting with a lightweight base image. For Java applications, you can choose:

  • openjdk:<version>-slim: A smaller JDK image based on Debian with fewer unnecessary packages.
  • openjdk:<version>-alpine: An even smaller JDK image based on Alpine Linux, though compatibility can sometimes be an issue with Alpine’s musl libc library.

Using a slim image instead of the full JDK image can save hundreds of MBs right from the start.

Example: Replace openjdk:17 with openjdk:17-slim to reduce base image size by approximately 200–300 MB.

2. Use Multistage Builds

With multistage builds, you can separate the build environment from the runtime environment. This approach is especially useful for Java applications, as you can compile your application in a full JDK environment and deploy it in a lighter runtime environment, such as a JRE (Java Runtime Environment).

Here’s a multistage build example with an OpenJDK image to illustrate this optimization.

#Stage 1: Build Stage with Full JDK
FROM openjdk:17 AS builder
WORKDIR /app

#Copy the build files and dependencies
COPY pom.xml .
RUN mvn dependency:go-offline

COPY src ./src
RUN mvn clean package -DskipTests

#Stage 2: Runtime Stage with Slim JRE
FROM openjdk:17-slim
WORKDIR /app

#Copy the built application from the previous stage
COPY --from=builder /app/target/myapp.jar /app/myapp.jar

#Set the entry point to run the application
ENTRYPOINT ["java", "-jar", "/app/myapp.jar"]

In this example:

  • The Build Stage compiles the application in a full openjdk:17 image and packages it into a .jar file.
  • The Runtime Stage uses openjdk:17-slim, a much smaller image that only includes the essentials required to run the .jar file.

3. Combine Commands to Minimize Layers

Each command in the Dockerfile creates a new layer, which can increase image size. Combining commands where possible helps minimize layers.

Example:

#Before optimization
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

#Optimized version
RUN apt-get update && apt-get install -y curl && apt-get clean

This combines all the RUN statements into a single layer, saving space by reducing layer count and clearing any caches immediately after installation.

4. Clean Up Temporary Files and Caches

For JDK images, temporary files from build tools (e.g., Maven or Gradle) are often retained, adding unnecessary weight. Removing these files right after the build process can make a big difference in image size.

For Maven-based applications, clean up the Maven repository cache after the build:

RUN mvn clean package -DskipTests && rm -rf ~/.m2/repository

This ensures that any unused dependencies or temporary build artifacts are removed, keeping only the required .jar file.

5. Use .dockerignore to Exclude Unnecessary Files

A .dockerignore file works similarly to .gitignore, preventing files and directories that don’t need to be in the image from being copied into the build context. This can help avoid accidentally adding large or sensitive files.

Example .dockerignore:

.git
target
*.md
.idea

This configuration excludes Git metadata, the target directory (where build artifacts go), and IDE files from the image, saving space.

6. Analyze Layers with docker history

Use the docker history <image_name> command to view each layer in your image and identify any unexpectedly large layers. This analysis can help pinpoint which parts of the Dockerfile are adding unnecessary size, guiding further optimizations.

Complete Example Dockerfile for Optimizing a JDK-Based Image

Here’s a comprehensive example Dockerfile that applies all of these tips for a Maven-based Java application. This approach can help bring down a typical Java Docker image size from 1GB to around 150MB.

#Build Stage
FROM openjdk:17 AS builder
WORKDIR /app

#Copy only the necessary files for dependency installation
COPY pom.xml ./
RUN mvn dependency:go-offline

#Copy the application source code and build it
COPY src ./src
RUN mvn clean package -DskipTests && rm -rf ~/.m2/repository

#Runtime Stage
FROM openjdk:17-slim
WORKDIR /app

#Copy only the compiled application from the build stage
COPY --from=builder /app/target/myapp.jar /app/myapp.jar

#Set the entrypoint to run the Java application
ENTRYPOINT ["java", "-jar", "/app/myapp.jar"]

Explanation of the Dockerfile:

  • Stage with Full JDK: The initial stage uses openjdk:17 to compile the application with Maven, storing the compiled output in a .jar file.
  • Runtime Stage with Slim JRE: The runtime stage uses openjdk:17-slim, a smaller image that only includes what’s necessary to execute the .jar file. This stage excludes the entire Maven setup and unnecessary files, drastically reducing the image size.

Expected Outcome By using this multistage build, you can cut down the image size significantly. This approach leverages a full JDK only for the build process and deploys with a JRE, allowing you to benefit from both the functionality of a complete JDK and the compactness of a minimal JRE image.

General Docker Image Optimization Tips

While we focused on JDK-specific optimization, the following tips apply to any Docker image:

  1. Update Base Images Regularly: Updated images often come with optimizations and security patches.
  2. Keep Images Small in CI/CD: Automated CI/CD workflows benefit from small images, as they are faster to download and deploy.
  3. Use Container Scanning Tools: Use tools like Docker’s docker scan to check for vulnerabilities and optimize images accordingly.
  4. Regularly Clean Up Local Images and Containers: Docker caching can leave old images on your local system, which consume disk space. Use docker system prune periodically to remove unused data.

Conclusion

Optimizing Docker images—especially JDK-based images—can significantly improve application deployment efficiency, reduce storage requirements, and accelerate build processes. By using multistage builds, lightweight base images, and Docker-specific techniques like layer and cache management, you can reduce a Java Docker image from over 1GB to around 150MB.

Following these best practices will not only streamline your Docker images but also lead to faster deployment cycles and more efficient resource usage. Start optimizing your Docker images today and experience the benefits of lean, performant containers.