Skip to main content

Docker multi-stage build for Spring Boot application

 


The post of this blog is to show the advantages of using multi-stage build on Docker for image size optimization. In case you've gotten here and never heard of the word "multi-stage", don't worry. Basically it is to build your image in different stages, and copy the artifacts from one stage to another, this allows you to keep the readability and maintenance of your Dockerfile, as well as reduce the size of the resulting image.

For practice, let's use this Spring Boot learning project here, in which we use Maven to compile our project. Then let's not wait any longer, and let's get to work.

Variant 1

This was the first variant that crossed my mind, basically it was, let me use a JDK base image to be able to compile the project, in this case openjdk:8, install Maven, compile the source code, and declare the entrypoint to the artifact binary .jar file.
 
 FROM openjdk:8  
 RUN apt-get update && apt-get install -y maven  
 COPY . /usr/app  
 WORKDIR /usr/app  
 RUN mvn clean package  
 EXPOSE 8080  
   
 ENTRYPOINT ["java","-jar","/usr/app/target/learning-spring-boot-0.0.1.jar"] 

Lets execute the build command:

 $ docker build -t josephrodriguez:variant1 -f ./Dockerfile . 

[+] Building 410.9s (10/10) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 32B
 => [internal] load .dockerignore
 => => transferring context: 35B
 => [internal] load metadata for docker.io/library/openjdk:8
 => [internal] load build context
 => => transferring context: 9.89kB
 => [1/5] FROM docker.io/library/openjdk:8
 => CACHED [2/5] RUN apt-get update && apt-get install -y maven
 => CACHED [3/5] COPY . /usr/app
 => CACHED [4/5] WORKDIR /usr/app
 => [5/5] RUN  mvn clean package
 => exporting to image
 => => exporting layers
 => => writing image sha256:ccce626033f48beb36087794b0664a97dc0e45ea95194310709c253b855449f0
 => => naming to docker.io/library/josephrodriguez:variant1
 

 The image was created successfully, but there was some curiosity in me to inspect and see the properties. 

$ docker inspect josephrodriguez:variant1 
{ ... "Size": 1002195280, "VirtualSize": 1002195280, ... "RootFS": { "Type": "layers", "Layers": [ "sha256:799760671c382bd2492346f4c36ee4033cf917400be4354c8b096ecef88df34b", "sha256:4e61e63529c26e95bef3cf769d07847dee7590b37b6b685186ce5c37a509b06d", "sha256:d00da3cd77634730ab5bc8e98b784cda40259f8be6a9b7138b4eee24ca1226b3", "sha256:8555e663f65b2a41e3aa61c0440f73bc5fac86c94d1cc14331d86d1c699382bf", "sha256:00ef5416d927e1fe6332a3c71e73d9b99eb25f0606bc8ecb0cbec4322d113a6d", "sha256:9cb5eb95298cec190a4f823f82c5a12bbec227e25c9ea4f3a91c05a0c06988a3", "sha256:d9b6ea8e7d5f87f028ab876890fc4129544152138293a167c7697386d57b8a79", "sha256:a5e66608e40747d1a4aba9b933c609b3091fc4eb9796b55c9a121476f4edd364", "sha256:ebd00c77f600344c1e8934b6e69f6dbdf4ae8b2cec1b875a422595a074bce142", "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", "sha256:cc22a4d2c89f478db3c784b65b1dd650a709a04456f971cecf41d20dac5f197e" ] } ... }

This information raised two major concerns:

  • The image size is really big
  • It´s contains too many layers
We all know that in itself each of the previous points goes against the performance of our image when deploy it on container.

Variant 2
In that direction we are going to move to try to solve these problems. So, we are going to propose solutions, what if:
  • We use a lighter image such as openjdk:8-jdk-alpine.
In that case we are unable to use apt command to install Maven and compile the project, but still we have a solution for this new problem. The Maven wrapper plugin on the repository can be used, we just need to set the executable permissions over it. In that direction our Dockerfile looks like follow:
 
 FROM openjdk:8-jdk-alpine
 COPY . /usr/app  
 WORKDIR /usr/app  
 RUN chmod +x mvnw && ./mvnw clean package  
 COPY /target/*.jar /app.jar
 EXPOSE 8080  
 ENTRYPOINT ["java","-jar","app.jar"]  

We are ready to rebuild our image with these new changes.

 
$ docker build -t josephrodriguez:variant2 -f ./Dockerfile .  

 [+] Building 482.1s (10/10) FINISHED  
  => [internal] load build definition from Dockerfile  
  => => transferring dockerfile: 262B  
  => [internal] load .dockerignore  
  => => transferring context: 35B  
  => [internal] load metadata for docker.io/library/openjdk:8-jdk-alpine  
  => [internal] load build context  
  => => transferring context: 10.13kB  
  => CACHED [basebuild 1/1] FROM docker.io/library/openjdk:8-jdk-alpine  
  => [build 1/4] COPY . /usr/app  
  => [build 2/4] WORKDIR /usr/app  
  => [build 3/4] RUN chmod +x mvnw && ./mvnw clean package  
  => [build 4/4] COPY /target/*.jar /app.jar  
  => exporting to image  
  => => exporting layers  
  => => writing image sha256:67a39c7c6ace2c2dfdc81db9909f58c008fb56c19fe75eb09ecc5c19f896fc8a  
  => => naming to docker.io/library/josephrodriguez:variant2  

Controlling a little anxiety and excitement to inspect our image again, we repeat the same step:

$ docker inspect josephrodriguez:variant2

{  
  "Size": 360337334,  
  "VirtualSize": 360337334,  
  ...  
  "RootFS": {  
   "Type": "layers",  
   "Layers": [  
    "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81",  
    "sha256:9b9b7f3d56a01e3d9076874990c62e7a516cc4032f784f421574d06b18ef9aa4",  
    "sha256:ceaf9e1ebef5f9eaa707a838848a3c13800fcf32d7757be10d4b08fb85f1bc8a",  
    "sha256:c16f37ef1693d02f9d1a938d965b131a3d9cd4f04de15a3b1cf000c251fb07bc",  
    "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef",  
    "sha256:ec3ae917e70014706981d1b58a02390d3d23b1fe5e94a0df5ecf2c73f04a2452",  
    "sha256:eea9c1935243ee73b7d6952bf110f2dfe93868897c3ca53452f4acfb907e2f8b"  
   ]  
  }  
  ...  
}  

Wow, much better. We have now a lighter image from approximately 1GB we have now 360 MB and with less layers. We are moving forward.

Now I have another curiosity with our progress, we are going to check the file system of our image.

 $ docker run -ti --entrypoint "/bin/sh" josephrodriguez:variant2  
 /usr/app # ls  
 Dockerfile  app.jar  data  mvnw  pom.xml  src  target  

In this new version of our image we see that we are publishing with it the source code inside the src folder, the Java byte code .class files and our app.jar file on target folder. What if we could have a lighter image with just our final artifact, the application .jar file?

Variant 3

In this direction we can think, what if we could divide the image process building into two phases? In a first stage we compile our source code and generate our app.jar file, and in a second stage we copy our app.jar file for the final image. We can use the openjdk:8-jdk-alpine image as a base for the first stage, and in the second stage we can use the openjdk:8-jre-alpine image as a base, which is even lighter because it only contains the tools to run our application and not it contains all the development tools.

Here I present multi-stage builds as our definitive solution, the Dockerfile would be as follows:

1:   FROM openjdk:8-jdk-alpine as build   
2:   COPY . /usr/app   
3:   WORKDIR /usr/app   
4:   RUN chmod +x mvnw && ./mvnw clean package   
5:      
6:   FROM openjdk:8-jre-alpine   
7:   COPY --from=build /usr/app/target/*.jar app.jar   
8:   EXPOSE 8080   
9:      
10:   ENTRYPOINT ["java","-jar","app.jar"]   

The code block from lines 1 to 4 is what we already know, now the magic comes in the following blocks.

On line 6 we are declaring that our base image is openjdk:8-jre-alpine instead of openjdk:8-jdk-alpine image.

 6:  FROM openjdk:8-jre-alpine   

In the next line, we are going to copy our app.jar file from our previous stage named build to our final image with the absolute path of the file.

 7:  COPY --from=build /usr/app/target/*.jar app.jar   

Lets build now our image.

 $ docker build -t josephrodriguez:variant3 -f ./Dockerfile .  

[+] Building 0.3s (12/12) FINISHED  
  => [internal] load build definition from Dockerfile  
  => => transferring dockerfile: 282B  
  => [internal] load .dockerignore  
  => => transferring context: 35B  
  => [internal] load metadata for docker.io/library/openjdk:8-jdk-alpine  
  => [internal] load metadata for docker.io/library/openjdk:8-jre-alpine  
  => [internal] load build context  
  => => transferring context: 10.15kB  
  => [build 1/4] FROM docker.io/library/openjdk:8-jdk-alpine  
  => [stage-1 1/2] FROM docker.io/library/openjdk:8-jre-alpine  
  => CACHED [build 2/4] COPY . /usr/app  
  => CACHED [build 3/4] WORKDIR /usr/app  
  => CACHED [build 4/4] RUN chmod +x mvnw && ./mvnw clean package  
  => CACHED [stage-1 2/2] COPY --from=build /usr/app/target/*.jar app.jar  
  => exporting to image  
  => => exporting layers  
  => => writing image sha256:601acb6abe23e60ca1fa6f1b11f03168f6581cd25df3ce58f378eb5a843b06c5  
  => => naming to docker.io/library/josephrodriguez:variant3  
 $ docker run -ti --entrypoint "/bin/sh" josephrodriguez:variant3  
 / # ls  
 app.jar bin    dev   etc    home   lib   media  mnt   opt     
 proc    root   run   sbin   srv    sys   tmp    usr   var  

Just what we want, in our image we only have our artifact app.jar. Lets inspect the image.

$ docker inspect josephrodriguez:variant3

[  
   {  
     "Config": {  
       "Env": [  
         "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/lib/jvm/java-1.8-openjdk/jre/bin:/usr/lib/jvm/java-1.8-openjdk/bin",  
         "LANG=C.UTF-8",  
         "JAVA_HOME=/usr/lib/jvm/java-1.8-openjdk/jre",  
         "JAVA_VERSION=8u212",  
         "JAVA_ALPINE_VERSION=8.212.04-r0"  
       ]  
     },  
     "Architecture": "amd64",  
     "Os": "linux",  
     "Size": 133549472,  
     "VirtualSize": 133549472,  
           ...  
     "RootFS": {  
       "Type": "layers",  
       "Layers": [  
         "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81",  
         "sha256:9b9b7f3d56a01e3d9076874990c62e7a516cc4032f784f421574d06b18ef9aa4",  
         "sha256:edd61588d12669e2d71a0de2aab96add3304bf565730e1e6144ec3c3fac339e4",  
         "sha256:0701526ec6789abb0b6b8454aaa6574edc5f8fd03bd27102ca51baf005e66425"  
       ]  
     }  
   }  
 ]  

First thing, our image is using as JAVA_HOME the JRE distribution. Second point, the size of our image decreased considerably and as a last advantage, we now have only 4 layers. I can't wish for anything better.

Let's compare the size of our images:
 $ docker image ls | grep variant  

 josephrodriguez       variant1         ccce626033f4        1GB  
 josephrodriguez       variant2         67a39c7c6ace        360MB  
 josephrodriguez       variant3         601acb6abe23        134MB  

In the Docker Hub repository we can even see that our compressed image is even lighter!!!!!


Recommended articules:

Comments

Popular posts from this blog

Generate self signed certificate with OpenSSL for IIS

Recently I wanted to enable SSL to a project hosted on IIS 8. Finally the tool I used was   OpenSSL , after many days fighting with   makecert   commands.The certificate is generated in Debian, but I could import it seamlessly into IIS 7 and 8. Download the  OpenSSL  compatible with your OS and setup the configuration file. Set the configuration file as default configuration of OpenSSL. # OpenSSL configuration file. # # Establish working directory. dir = . [ ca ] default_ca = CA_default [ CA_default ] serial = $dir/serial database = $dir/certindex.txt new_certs_dir = $dir/certs certificate = $dir/cacert.pem private_key = $dir/private/cakey.pem default_days = 365 default_md = md5 preserve = no email_in_dn = no nameopt = default_ca certopt = default_ca policy = policy_match [ policy_match ] countryName = match stateOrProvinceName

Configure SSL on MS SQL Server with OpenSSL

I configured successfully SSL on Microsoft SQL Server 2012 Express Edition for the purpose of encrypting external network connections to the database that are made through Internet. For performance reasons for internal clients on the network I do not want to force the use of SSL and leave to the clients the option of use it or not. I set   Force Encryption   to   No   with the following steps: Sql Server Configuration Manager Sql Server Network Configuration Protocols for (MYSQLSERVERNAME) Right click:  Properties Flags  tab. When I try to establish an encrypted connection with Microsoft Sql Server Management Studio checking  Encrypt connection  option on  Options  >  Connection Properties  I get the following error. A connection was successfully established with the server, but then an error occurred during the login process. (provider: SSL Provider, error: 0 - The target principal name is incorrect.) (Microsoft SQL Server, Error: -2146893022) What is striking is that if I select