# Creating Docker Image of Spring Boot Application using Buildpacks

# Introduction

You have created a [Spring Boot](https://spring.io/projects/spring-boot) application. It is working great on your local machine and now, you need to deploy the application somewhere else. On some platforms, you can directly submit the `jar` file and it will be deployed. At some places, you can spin up a virtual machine, download the source code there, build it, and run it. But, most of the time you will need to deploy the application using containers. Most of the time, [Docker](https://www.docker.com/) is used to build and run the image in a container. Also, when you upload the `jar` file to some platforms, the application is run inside a container under the hood.

So, in this blog, we will see `3` different ways to build a Docker image for the given Spring Boot application. Let's start:

# Basic Container Image

The naive and insufficient way to build the Docker image for any application is to use a simple [Dockerfile](https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/) which copies the `jar` file inside the image and run it using `java -jar` command.

## Create Dockerfile

Here is the `Dockerfile` which you can put at the root of the project:

```Dockerfile
FROM eclipse-temurin:21-jre-ubi9-minimal

ARG JAR_FILE

COPY ${JAR_FILE} application.jar

ENTRYPOINT ["java", "-jar", "/application.jar"]
```

We have specified one argument `JAR_FILE` which is the location of the `jar` file to use.

## Building Docker Image

After creating the above `Dockerfile`, below steps are used to create the Docker image:

1. Build the `jar` file for Spring Boot project:
    
    ```bash
    ./gradlew bootJar # For Gradle build system
    ```
    
    OR
    
    ```bash
    ./mvnw spring-boot:build-jar # For Maven build system
    ```
    
2. Use the `Dockerfile` to build the Docker image using the latest `jar` file. In the below command replace the `{IMAGE_NAME}` with the required image name and `{JAR_FILE}` with the path to the generated `jar` file. The image name contains a tag as well, like - `mycompany/product-service:0.0.1-SNAPSHOT`:
    
    ```bash
    docker build --build-arg JAR_FILE={JAR_FILE} --tag {IMAGE_NAME} .
    ```
    
3. Verify if the Docker image is built using the following command. You should be able to see the image with the name specified in the command above:
    
    ```bash
    docker images
    ```
    

# Efficient Container Image using Layered Jar

While it is possible and easy to package a Spring Boot uber `jar` as a Docker image (as mentioned in the previous method), there are many downsides to copying and running the fat `jar` as-is in the Docker image. For instance,

* There is some extra overhead when running uber `jar` without unpacking it.
    
* Putting the application's code and all of its dependencies in a single layer is not optimal.
    

Since we compile our code more often than upgrading the Spring Boot version, it is better to separate things a bit more. If we put those `jar` files (which are rarely changed) in the layer before the application layer, then Docker often needs to change only the bottom layer and can pick the rest from its cache.

## Enable Layered Jar

To create a layered Docker image, we need to create a **layered jar** first. Nowadays, it is enabled by default in Gradle and Maven. You can enable or disable the layered jar behavior using the following setting:

```java
// build.gradle
tasks.named("bootJar") {
	layered {
		enabled = false
	}
}
```

```kotlin
// build.gradle.kts
tasks.named<BootJar>("bootJar") {
   layered {
      enabled.set(false)
   }
}
```

```xml
<!-- pom.xml -->
<project>
   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
               <layers>
                  <enabled>true</enabled>
               </layers>
            </configuration>
         </plugin>
      </plugins>
   </build>
</project>
```

You can even tune how the layers are created. See the documentation for [gradle](https://docs.spring.io/spring-boot/gradle-plugin/packaging.html#packaging-executable.configuring.layered-archives) or [maven](https://docs.spring.io/spring-boot/maven-plugin/packaging.html#packaging.layers) configuration.

## Create Dockerfile

Below is the `Dockerfile` that can be used to take advantage of layered `jar` and to create layered Docker images of the Spring Boot application.

```Dockerfile
# Perform the extraction in a separate builder container
FROM eclipse-temurin:21-jre-ubi9-minimal AS builder

WORKDIR /builder

# This points to the built jar file in the target folder
# Adjust this to 'build/libs/*.jar' if you're using Gradle
ARG JAR_FILE=target/*.jar

# Copy the jar file to the working directory and rename it to application.jar
COPY ${JAR_FILE} application.jar

# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar application.jar extract --layers --destination extracted

# This is the runtime container
FROM eclipse-temurin:21-jre-ubi9-minimal

WORKDIR /application

# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./

# Start the application jar - this is not the uber jar used by the builder
# This jar only contains application code and references to the extracted jar files
# This layout is efficient to start up and CDS friendly
ENTRYPOINT ["java", "-jar", "application.jar"]
```

## Building Docker Image

The steps to build the layered Docker image are the same as building a basic Docker image. Please refer there.

# Cloud Native Buildpacks

What if I tell you that you can create a Docker image without creating `Dockerfile`?. We can build docker images directly from the Gralde or Maven plugin using [Cloud Native Buildpacks](https://buildpacks.io/). Some platforms (like [Heroku](https://www.heroku.com/) or [Cloud Foundry](https://www.cloudfoundry.org/)) use Buildpacks to convert provided `jar` files into runnable images.

Spring Boot includes buildpack support directly for Maven and Gradle. We don't need to include any additional plugins. Just run the below command:

```bash
./gradlew bootBuildImage # For gradle build system
```

OR

```bash
./mvnw spring-boot:build-image # For maven build system
```

The above command generates an image with the default name `{PROJECT_NAME}:${PROJECT_VERSION}`. If you want to configure the name of the generated image, you can follow the below steps:

## Configure image name for Gradle build system

We can configure `bootBuildImage` task to set the name of the image, like this:

```kotlin
// For build.gradle.kts
val imagePrefix = "javarush"
val dockerImageName = "docker-example"
tasks.named<BootBuildImage>("bootBuildImage") {
   imageName.set("${imagePrefix}/${dockerImageName}:${version}")
}
```

```java
// For build.gradle
def imagePrefix = "javarush"
def dockerImageName = "docker-example"
tasks.named("bootBuildImage") {
   imageName = "${imagePrefix}/${dockerImageName}:${version}"
}
```

## Configure image name for Maven build system

We can configure `spring-boot-maven-plugin` to use another image name, like this:

```xml
<properties>
   <imagePrefix>javarush</imagePrefix>
</properties>

...

<project>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<image>
						<name>${imagePrefix}/${project.artifactId}:${project.version}</name>
					</image>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
```

## Configure the image name while running the command

We can even define the name of the image while running the command to build the image.

```bash
./gradlew bootBuildImage --imageName=javarush/docker-example:1.0.0 # For grade build system

./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=javarush/docker-example:1.0.0 # For maven build system
```

You can see the documentation to further configure the [Gradle](https://docs.spring.io/spring-boot/gradle-plugin/packaging-oci-image.html) or [Maven](https://docs.spring.io/spring-boot/maven-plugin/build-image.html#build-image) plugin. This is my go-to method to create a Docker image for any Spring Boot application.

# Running Docker Container

Once you create a docker image, you need to make sure that it works as expected. After you make sure that the image is created, you can directly run it using the `docker run` command. For example,

```bash
docker run -p "8080:8080" {IMAGE_NAME}
```

But, this is not how images are used in production applications. [Docker Compose](https://docs.docker.com/compose/) is used to run and manage multiple docker images.

# Conclusion

In this blog, we have seen how to build Docker images for Spring Boot applications using different methods. Being able to build docker images for your apps is a must skill to know because the image is what gets delivered. Thanks for reading the article till the end. I appreciate it. I will meet you in the next one. As always, all feedback and suggestions are welcome.
