
Build and Run a Spring Boot Application with Docker
In this post, we’ll use Docker to create an image of a Spring Boot application and run it in a container. We’ll start with a single Dockerfile, then we’ll also cover how to scale our application using Docker Compose, and we’ll see how to build our Java code using Docker.
- The Hola Spring Boot application
- Dockerizing Spring Boot (or any executable .jar file)
- Running a Spring Boot application using docker-compose
- Scaling up the Spring Boot app using docker-compose
- Building a Spring Boot app using Docker
- Summary
The Hola Spring Boot application
The application we’ll employ through this post is pretty simple: a Spring Boot Web application with a REST controller giving us a warm Spanish greeting. Apart from the ‘Hola’, we’ll show from which IP address the greeting is coming from. That will be useful for us to see how Docker works.
Creating an example controller
I’ll guide you through this post as if you were creating the application from scratch, but you can also clone the existing repository and play with it.
First, let’s create the application skeleton using the Spring Boot Initializr: http://start.spring.io, including ‘Web’ as the only dependency and choosing Maven (to follow the same instructions as in this post). Then, we code this simple REST Controller:
package com.thepracticaldeveloper.hola;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.InetAddress;
import java.net.UnknownHostException;
@RestController
public final class HolaController {
@GetMapping
public final String hola() throws UnknownHostException {
return "Hola! Puedes encontrarme en " + InetAddress.getLocalHost().getHostAddress();
}
}
Since there is no specified path, that text is returned when we perform a GET request to the root context. Let’s give it a try.
Running the application
Use your local Maven installation or the wrapper (mvnw
command) normally included by the Spring initializer. From the application’s root folder, execute:
mvn spring-boot:run
The Spring Boot application should start and show a message similar to this one:
Started HolaApplication in 2.198 seconds (JVM running for 5.628)
If you navigate with your browser (or use command line tools like curl or httpie) to localhost:8080
, you’ll see the response from our friendly app (the IP may vary of course):
Hola! Puedes encontrarme en 192.168.0.21
As an alternative, you can also run the application by first packaging it in a jar file, and then run it with the java command. In that case, use Maven to create the package:
mvn clean package
The resulting .jar
file will be placed in a new target
folder. Now you can execute this command and you’ll get the application running as before:
java -jar ./target/hola-docker-1.0.0-SNAPSHOT.jar
Understanding the basics
So far there is nothing to do with Docker, but it’s important to highlight a couple of concepts to fully understand the rest of this post:
- You used your machine to build the application (using Maven, either previously installed or the wrapper).
- You used your machine to run the application (either using the spring-boot plugin or the jar file).
Dockerizing Spring Boot (or any executable .jar file)
Let’s start playing with Docker. If you haven’t done it yet, you need to install Docker in one of its versions (Windows, Mac, Linux). If you use an old Windows or Mac system, you’ll need to use the Docker Toolbox. In that case, please note that when I refer to localhost
, you should replace it with the IP of your VM (in which Docker runs).
To keep the learning path as smooth as possible, I’ll go through several steps that will show you different things you can do with Docker and a java application:
- Use a Dockerfile to define an image, then build it and run it with plain Docker commands.
- Use a
docker-compose.yml
and its command line interface to extend the functionalities when running multiple containers, and simplify it by having a predefined configuration. - Use docker to build our java code as well (note that this is more an experiment than a real-life case).

Minimal configuration: Dockerfile
Dockerfile for Spring Boot
The minimal requirement we have to run our Spring Boot app in a container is to create a file named Dockerfile
with instructions to build the image. Then we’ll run a Docker container using that image.
FROM openjdk:10-jre-slim
COPY ./target/hola-docker-1.0.0-SNAPSHOT.jar /usr/src/hola/
WORKDIR /usr/src/hola
EXPOSE 8080
CMD ["java", "-jar", "hola-docker-1.0.0-SNAPSHOT.jar"]
As you see, I kept it really simple. The image is based on a slim Linux with JRE 10; on top of that we copy the JAR file, we change to the working directory in which that package is, and we execute the same command as we did before when running from our machine. The EXPOSE
instruction is telling Docker that the 8080 port can be exposed outside the container, but note that we’ll also need to make the port available when running it anyways. If you want further details about what every line does, I recommend you to have a look at the Dockerfile reference documentation.
If you use Java 8 instead, you can benefit from an even smaller Linux image (alpine
). Unfortunately, the alpine distribution is not yet compatible with Java 10 at the time of writing this post.
Building the image
We build the image so it’ll be available in our local Docker registry. We give it a name with the -t
flag, and specify the current directory as the one in which the Dockerfile
lives.
docker build -t hola-manual-build .
The command will output the status of every step while building the image. It will download the base image if it’s not available yet in your existing docker images, so be patient. If we now run the command docker images
to list the available images in our registry, we’ll see the new one:
REPOSITORY TAG IMAGE ID CREATED SIZE
hola-manual-build latest 190f9d37da59 7 hours ago 302MB
Running the Spring Boot application with Docker
We’re almost there to have our application up and running on Docker. We just need to create a container using the new image:
docker run -p 8000:8080 hola-manual-build
By using the -p
flag we’re telling docker to expose the container’s port 8080 (on the right of the colon) on the host’s port 8000 (on the left, our machine). We can access from our machine to localhost:8000
(you can also use your browser) and see the greeting message again, this time coming from the Docker container:
moises$ http localhost:8000
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 10:05:01 GMT
Hola! Puedes encontrarme en 172.17.0.2
Note that the IP is different from the previous one since now the application is deployed inside a Docker container. Each container will get a new assigned IP inside the Docker’s network.
Dockerfile: Recap
We did it! We have now the java Spring Boot application running in a Docker container. Note the difference with the previous case:
- You used your machine to build the application (using Maven, either previously installed or the wrapper).
- You used Docker to run the application:
- You built the image using
docker build
. - You ran the container using
docker run
, specifying the port to make it available from the host.
- You built the image using
Running a Spring Boot application using docker-compose
Docker Compose is a tool to run multiple containers, define how they are connected, how many instances should be deployed, etc. It allows you to define how the image should be built as well. Let’s use it in our scenario to simplify the way we run the container.
Defining the docker-compose file
We create a file docker-compose.yml
with this content:
version: '2.2'
services:
hola:
build:
context: ./
dockerfile: Dockerfile
image: holaweb
ports:
- 8080
networks:
- network1
networks:
network1:
We create only one service: hola
. We define how it’s built: using the same folder as a context, since there is the Dockerfile
and the different files located. We don’t need to specify the dockerfile parameter, but we’ll introduce it here since we plan to modify it later. We give a name to the image so, if it’s not present, the compose tool will build it and assign that name to the resulting image. If it’s there, it won’t build the image again unless we instruct it to do so (we’ll see how later).
Building and Running the application
Note that we’re using ports
to make the container port 8080 available from outside. In this case, we’re not specifying the host port, so Docker will pick a random one. Let’s execute docker-compose and see what happens:
$ docker-compose up
This should be the output of the command:
$ docker-compose up
Building hola
Step 1/5 : FROM openjdk:10-jre-slim
---> 65f8e952b8a9
Step 2/5 : COPY ./target/hola-docker-1.0.0-SNAPSHOT.jar /usr/src/hola/
---> 4b9605f98946
Step 3/5 : WORKDIR /usr/src/hola
Removing intermediate container 4a9ec666872f
---> 0e487bfb3314
Step 4/5 : EXPOSE 8080
---> Running in 4765084c3829
Removing intermediate container 4765084c3829
---> 92e311cc1504
Step 5/5 : CMD ["java", "-jar", "hola-docker-1.0.0-SNAPSHOT.jar"]
---> Running in c2a9cd73bb8b
Removing intermediate container c2a9cd73bb8b
---> ada28f26d28a
Successfully built ada28f26d28a
Successfully tagged holaweb:latest
WARNING: Image for service hola was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating spring-boot-hola-docker_hola_1 ... done
Attaching to spring-boot-hola-docker_hola_1
hola_1 |
hola_1 | . ____ _ __ _ _
hola_1 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
hola_1 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
hola_1 | \\/ ___)| |_)| | | | | || (_| | ) ) ) )
hola_1 | ' |____| .__|_| |_|_| |_\__, | / / / /
hola_1 | =========|_|==============|___/=/_/_/_/
hola_1 | :: Spring Boot :: (v2.0.3.RELEASE)
hola_1 |
hola_1 | [Application logs...]
As you can see, the first step in docker-compose
builds the image, since there is no holaweb
image available. It will tag it with that name, and then run it according to the rest of the details in the docker-compose.yml
file. It will expose the container’s port to the host in a port that we don’t know yet. Let’s find it out (you need to open a new terminal):
$ docker-compose ps
And here is the output of that command:
Name Command State Ports
------------------------------------------------------------------------------
hola_hola_1 java -jar hola-docker-1.0. ... Up 0.0.0.0:32768->8080/tcp
So now we know that the host port is 32768 (it may vary in your case). We navigate to localhost:32768
to have our greeting, again coming from a container.
Hola! Puedes encontrarme en 172.20.0.2
These are some remarks about this solution:
- Subsequent executions of
docker-compose up
won't require building the image. - However, you can rebuild the image whenever you want, by using:
-
docker-compose build
, which will build all the services but not run them. -
docker-compose up --build
, which will build all the services and then run them.
-
Docker-compose: Recap
Let’s summarize what we got so far with this docker-compose
solution:
- You used your machine to build the application (using Maven, either previously installed or the wrapper).
- You used
docker-compose
to build and run the application.- You built and run the container in one command, using
docker-compose up
- You built and run the container in one command, using
Scaling up the Spring Boot app using docker-compose
Changes in docker-compose
We can use docker-compose
to run multiple container instances of the same image. We prepared for that already, by leaving Docker to assign a random port in the host for our application, so we only need to add one line to our docker-compose.yml
file to run multiple instances (in this case 3):
version: '2.2'
services:
hola:
build:
context: ./
dockerfile: Dockerfile
image: holaweb
ports:
- 8080
networks:
- network1
scale: 3
networks:
network1:
Just by adding that scale: 3
line, we’ll get three containers up and running when we run:
$ docker-compose up
Listing the multiple Docker instances
You’ll see in the output how the three containers with the three Spring Boot applications start in parallel. In a new terminal, let’s now execute:
$ docker-compose ps
So we can list what are the ports exposed from Docker. In my case, this is the output:
$ docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------
hola_hola_1 java -jar hola-docker-1.0. ... Up 0.0.0.0:32774->8080/tcp
hola_hola_2 java -jar hola-docker-1.0. ... Up 0.0.0.0:32773->8080/tcp
hola_hola_3 java -jar hola-docker-1.0. ... Up 0.0.0.0:32775->8080/tcp
You can see how easy is to have multiple instances of our containers up and running. You can also override the number of instances from the command line, by running docker-compose up --scale holaweb=[number_of_instances]
.
Testing
If we do the GET requests to the three different ports, we’ll see how each container responds with a different IP in their greeting:
moises$ http localhost:32774
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 11:11:34 GMT
Hola! Puedes encontrarme en 172.20.0.3
moises$ http localhost:32773
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 11:12:08 GMT
Hola! Puedes encontrarme en 172.20.0.2
moises$ http localhost:32775
HTTP/1.1 200
Content-Length: 38
Content-Type: text/plain;charset=UTF-8
Date: Sun, 10 Dec 2017 11:12:11 GMT
Hola! Puedes encontrarme en 172.20.0.4
If you’re experienced with topics such as service discovery and routing, you must be visualizing now what are the possibilities that docker brings. We could include in this scenario a service registry and an API gateway and benefit from load balancing without too much effort. I’ll try to cover that in a different post.
Building a Spring Boot app using Docker
Introduction
This is a more advanced scenario in which we use a docker container to build the application and also to run it. Don’t do this unless you have a good reason; most of the modern Continuous Integration tools use Docker themselves to create dynamic containers to build the application packages, run the tests, etc.
Anyway, I wanted to cover this since it’s a good showcase of the possibilities of Docker and it can make sense in some specific situations:
- You want to build locally but you're working on a machine that doesn't fit the requirements to build the application. For example, you need linux / mac tools but you're running on windows.
- Your application requires a complex setup to be built so it's convenient to have a pre-cooked docker image with all the required packages and configuration that you can run anywhere.
However, note that for the scenario shown here it’s easier to build the code as we’ve been doing until now, just using java and maven on our machine while building the application and then docker to run it. Anyway, we’ll use it as a reference so you can get the idea and apply it (if needed) to other cases.

Dockerfile to build Java code and run it
Let’s create a different Dockerfile, which we’ll name Dockerfile-build
:
FROM maven:3.5.4-jdk-10-slim
WORKDIR /usr/src/java-code
COPY . /usr/src/java-code/
RUN mvn package
WORKDIR /usr/src/java-app
RUN cp /usr/src/java-code/target/*.jar ./app.jar
EXPOSE 8080
CMD ["java", "-jar", "app.jar"]
These are the most important remarks:
- Instead of using a JRE as the base image, we're using one with Maven on it. That one is a layer on top of the Java image, so both tools will be available.
- The first part of the Dockerfile is copying the java code to a folder in a container and running
mvn package
. Remember that this step will be executed *while building the image* and, as a result, the jar file will be committed to that image layer inside a newtarget
folder, as expected when we run that maven command. - The second part of the Dockerfile is similar to the one presented above. The only difference is that now we're not taking the jar file from the host's context, but using it from the previous image step.
- Since we're now copying all the contents of the directory to the docker image, it makes sense to create a `.dockerignore` file to avoid copying things we don't need there (see the repository for an example).
Combine it from docker-compose
We reuse our existing docker-compose.yml
file to build the image with this new approach, changing the line pointing to the Dockerfile to be
dockerfile: Dockerfile-build
Then we execute:
$ docker-compose build
We’ll see how the build process is packaging the java application, downloading all required dependencies and running the tests. The subsequent steps to build the image are taking the resulting .jar
file and copying to a different folder.
As you might expect, we can now run the container with the same result as before, using docker-compose up
.
What are the differences?
Let’s use the same structure as before (Recap sections) to show you what happened in this case:
- You used docker to build the application (so you don’t need to have Maven nor Java installed on your machine).
- You used docker-compose to build and run the application.
- You built and run the container in one command, using
docker-compose up
- You built and run the container in one command, using
That could be seen as an advantage but remember my previous comments: that scenario has just a few real-life applications.
Another noticeable difference is the docker image size. If we run docker images
with the image built from the first dockerfile, I get a result around 300 MB:
REPOSITORY TAG IMAGE ID CREATED SIZE
holaweb latest ada28f26d28a 12 minutes ago 302MB
However, if we do that after building the image with Dockerfile-build
, the image size gets quite bigger:
REPOSITORY TAG IMAGE ID CREATED SIZE
holaweb latest e3330a5ad9cb 19 seconds ago 482MB
It’s the expected result since now our image has more layers and also includes the source code in it.
Accessing the source code
The last difference I want to point out is that you can access the source code when running a container. To do that, you can attach a bash terminal to a running container, for instance, when running multiple instances, we can do this:
moises$ docker-compose exec --index=1 hola /bin/bash
root@77fcfddd5402:/usr/src/java-app# ls -l /usr/src
total 8
drwxr-xr-x 1 root root 4096 Jul 8 17:04 java-app
drwxr-xr-x 1 root root 4096 Jul 8 17:04 java-code
root@77fcfddd5402:/usr/src/java-app# ls /usr/src/java-code/
Dockerfile README.md pom.xml target
Dockerfile-build docker-compose.yml src
root@77fcfddd5402:/usr/src/java-app#
How can CI tools help here?
Even though this is a working experiment, there are only a few real-life scenarios in which building your Java application with Docker by yourself might be useful. If you don’t need a complex setup on your machine to build the application, you can rely on tools like Jenkins Pipelines, CircleCI, GitLab, etc.
These tools already build your apps in Docker containers out-of-the-box, so you just need to configure them properly and use their pipeline definition language to specify how to build your code - in this case, using maven.
Summary
In this post, we went through different steps to see how we can containerize a Java Spring Boot application using Docker.
- First, we created a simple
Dockerfile
and we built the image and run it inside a container using plain docker commands. - After that, we introduced
docker-compose
and saw how it can help to simplify the setup and execution. This would be even more visible when multiple services coexist. We also covered how to easily scale up our service. - Last, we had a look at the possibility of building the application using Docker too and reviewed the reasoning for doing that.
I hope you found the guide useful. Use a comment or the Facebook page to give me some feedback!
Comments