How to reduce my java/gradle docker image size?
Asked Answered
D

6

12

I have a Docker file like the following:

FROM openjdk:8

ADD . /usr/share/app-name-tmp

WORKDIR /usr/share/app-name-tmp

RUN ./gradlew build \
    mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar

WORKDIR /usr/share/app-name

RUN rm -rf /usr/share/app-name-tmp

EXPOSE 8080

RUN chmod +x ./docker-entry.sh

ENTRYPOINT [ "./docker-entry.sh" ]

The problem is that the final image size is 1.1GB, I know it happens because gradle downloads and stores all the dependencies. What is the best way to remove those unnecessary files and just keep the jar?

Disrespectable answered 4/12, 2016 at 11:23 Comment(0)
U
5

Each RUN instruction creates a new layer on top of the existing file system. So the new layer after RUN instruction that deletes you app-name-tmp directory just masks the previous layer containing the downloaded libraries. Hence your docker image still has that size from all the layers built.

Remove the separate RUN rm -rf /usr/share/app-name-tmp instruction and include it in the same RUN instruction that does gradle build as shown below.

RUN ./gradlew build \
    mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar \
    rm -rf /usr/share/app-name-tmp/*

So, your final Dockerfile would be

FROM openjdk:8

ADD . /usr/share/app-name-tmp
WORKDIR /usr/share/app-name-tmp

RUN ./gradlew build \
    mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar \
    rm -rf /usr/share/app-name-tmp/*

WORKDIR /usr/share/app-name

EXPOSE 8080
RUN chmod +x ./docker-entry.sh
ENTRYPOINT [ "./docker-entry.sh" ]

The image built will still add up size from the directory /usr/share/app-name-tmp.

Underwent answered 4/12, 2016 at 13:49 Comment(3)
The base image could be contributing a little bit, but this is the real answer. Unfortunately, Docker has not addressed the "nested builds". The way I have generally dealt with this is having separate "build" and "runtime" images to handle the Docker limitations. See this article for a possible way of dealing with the problem.Shirleyshirlie
@Shirleyshirlie you can now use multistage builds to address that issue docs.docker.com/engine/userguide/eng-image/multistage-buildZins
@GuidoGarcía good point! Could be a good opportunity for a new answer or to improve upon this one.Shirleyshirlie
R
7

I am really confused about your image size. I have typical Spring Boot applications offering a REST service including an embedded servlet container in less than 200MB! It looks like your project dependencies can and should be optimised.

Docker Image

The openjdk:8 (243MB compressed) can be replaced by one with a reduced Alpine unix image like openjdk:8-jdk-alpine (52MB) as a base image but if you don't need compiler capabilities (e.g. don't use JSPs) you may also go for openjdk:8-jre-alpine (42MB) which includes the runtime only, have a look into Docker Hub. I use that for Spring Boot based REST services working great.

Java Dependencies

The Java dependencies needed for compile and runtime have to be included but you may have unused dependencies included:

  • check your dependencies, are the current compile/runtime dependencies really used or maybe can be removed or moved to test, see Gradle Java Plugin
  • some dependencies have a lot of transitive dependencies (display using gradle dependencies), check out for unnecessary ones and exclude them if unused, see Gradle Dependency Management. Be sure to do integration tests before applying finally, some transitive dependencies are not well documented but may be essential!
Radiology answered 4/12, 2016 at 11:46 Comment(2)
If I execute ./gradlew build, the resulting jar is only 95MB, but the image size is around 1GBDisrespectable
Why you are running gradle from docker and what is in the directories you add? I use gradle docker plugin and only add the jar to the image calling docker from gradle and not the other way aroundRadiology
Z
7

With Docker 17.05+ you can use multi-stage builds.

"With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image."

So your Dockerfile could look like this:

#
# first stage (build)
#
FROM openjdk:8 as build

ADD . /usr/share/app-name-tmp

WORKDIR /usr/share/app-name-tmp

RUN ./gradlew build && \
    mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar

#
# second stage. use alpine to reduce the image size
#
FROM openjdk:8-jre-alpine

WORKDIR /usr/share/app-name

COPY --from=build /usr/share/app-name/app-name.jar .

EXPOSE 8080

RUN chmod +x ./docker-entry.sh

ENTRYPOINT [ "./docker-entry.sh" ]

This way you only keep the jar and all the unnecessary files are not included in the final image.

Zins answered 17/9, 2017 at 22:31 Comment(1)
Shouldn't it be COPY --from=build as the first stage is named build (not builder)?Deary
U
5

Each RUN instruction creates a new layer on top of the existing file system. So the new layer after RUN instruction that deletes you app-name-tmp directory just masks the previous layer containing the downloaded libraries. Hence your docker image still has that size from all the layers built.

Remove the separate RUN rm -rf /usr/share/app-name-tmp instruction and include it in the same RUN instruction that does gradle build as shown below.

RUN ./gradlew build \
    mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar \
    rm -rf /usr/share/app-name-tmp/*

So, your final Dockerfile would be

FROM openjdk:8

ADD . /usr/share/app-name-tmp
WORKDIR /usr/share/app-name-tmp

RUN ./gradlew build \
    mv ./build/libs/app-name*.jar /usr/share/app-name/app-name.jar \
    rm -rf /usr/share/app-name-tmp/*

WORKDIR /usr/share/app-name

EXPOSE 8080
RUN chmod +x ./docker-entry.sh
ENTRYPOINT [ "./docker-entry.sh" ]

The image built will still add up size from the directory /usr/share/app-name-tmp.

Underwent answered 4/12, 2016 at 13:49 Comment(3)
The base image could be contributing a little bit, but this is the real answer. Unfortunately, Docker has not addressed the "nested builds". The way I have generally dealt with this is having separate "build" and "runtime" images to handle the Docker limitations. See this article for a possible way of dealing with the problem.Shirleyshirlie
@Shirleyshirlie you can now use multistage builds to address that issue docs.docker.com/engine/userguide/eng-image/multistage-buildZins
@GuidoGarcía good point! Could be a good opportunity for a new answer or to improve upon this one.Shirleyshirlie
B
2

It seems your image comes from

FROM openjdk:8

so from

https://github.com/docker-library/openjdk/blob/e6e9cf8b21516ba764189916d35be57486203c95/8-jdk/Dockerfile

and in fact a Debian

FROM buildpack-deps:jessie-scm

you should try to use an Alpine base

https://github.com/docker-library/openjdk/blob/9a0822673dffd3e5ba66f18a8547aa60faed6d08/8-jdk/alpine/Dockerfile

I guess your image will be at least half the size

Bradleigh answered 4/12, 2016 at 11:35 Comment(1)
I know about Alpine, but I want to keep using the 'openjdk:8' image. It is ~ 650MB but my application is adding around 500MB more in the ./gradlew build step.Disrespectable
S
1

Is this the container you deploy to production? If so, don't use it for the actual build. Do the build (and the testing) elsewhere and once it is blessed, copy just the JAR to your Docker production container.

Sexagenarian answered 5/12, 2016 at 4:7 Comment(0)
C
1

For OpenJDK-12

My application is written in Kotlin along with spring boot and maven.

I had same issue with openJDK-12 and OracleOpenJDK-12 size is 470 MB.

I wanted to reduce my container size so i selected adoptopenjdk/openjdk12:x86_64-alpine-jre-12.33 and achieved 189 MB as shown below.

FROM adoptopenjdk/openjdk12:x86_64-alpine-jre-12.33
RUN mkdir /app
COPY ./target/application-SNAPSHOT.jar /app/application-SNAPSHOT.jar
WORKDIR /app
CMD ["java", "-jar", "application-SNAPSHOT.jar"]

My final container size is 189MB (34 MB Application Jar size + 155 MB Base image size.)

Chevet answered 22/7, 2019 at 5:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.