A Start to Finish Guide to Docker With Java, Part 1
In this tutorial, we'll package a Spring Boot application, run it, and look at the different commands for managing images and containers.
Join the DZone community and get the full member experience.
Join For FreeIntro to Managing and Running a Containerized Java Spring Boot Application
Docker is a platform for packaging, deploying, and running applications in containers. It can run containers on any system that supports the platform: a developer's laptop, systems on "on-prem," or in the cloud without modification. Images, the packages Docker uses for applications, are truly cross-platform.
Java microservices are a good target for Docker. Running a service in a container augments development with a common target platform for development, testing, and deployment. Containers are also an excellent first step toward moving to a flexible and cost-effective cloud architecture.
In this tutorial, we'll package a Spring Boot application, run it, and look at the different commands for managing images and containers.
Docker Setup and Installation
Install Docker
First, start by installing the Docker desktop tools found here. Download the correct installer for your operating system and follow the instructions.
Next, verify the installation with the following two commands:
$ docker --version
Docker version 18.03.1-ce, build 9ee9f40
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
9bb5a5d4561a: Pull complete
Digest: sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d59bde73361e77
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
....
The first command checks docker's version. Your version may be different, depending on your platform.
Docker run hello-world
does what it sounds like - it runs an image named hello-world.
First, it looks for this image on the local system. Since it is not there it downloads it from Docker Hub. Then it runs the container, which displays a message telling us everything's working fine, and then it spells out the process it took to run the image.
Docker looked for a local copy of the hello-world image. Since it wasn't present, it went to Docker Hub and downloaded the latest image. Once the image was completely downloaded, it ran hello-world in a container.
Spring Boot Application
To keep the tutorial focused on Docker, we'll use an existing project, which is explained in this Spring tutorial. It's a small web application that manages employee records.
You can run the application with this command line:
java -Dspring.profiles.active=default -jar target/spring-boot-ops.war
It serves a single page at http://localhost:8080 /springbootapp/employees:
Let's get right to work running this in Docker.
Build and Run a Docker Application
Building an Image
You create images with a Dockerfile, which lists the components and commands that make up the package.
First, create the file:
# Alpine Linux with OpenJDK JRE
FROM openjdk:8-jre-alpine
# copy WAR into image
COPY spring-boot-app-0.0.1-SNAPSHOT.war /app.war
# run application with this command line
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", "/app.war"]
Dockerfiles are a list of commands that Docker performs to build an image. We'll take a closer look at these commands below.
It's a best practice to build images in a "clean" directory, as docker build's default behavior is to copy the working directory to the image. Place this file in a new folder at the top of your project named docker.
You can't use relative paths in Dockerfiles, so you'll need to modify your pom.xml to place the war file in the target directory.
Next, add the output directory property to the spring-boot-maven-plugin.
This copies the jar into the docker directory as part of the package build target. Make sure your pom.xml has this block in the plugins section:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
<configuration>
<mainClass>com.stackify.Application</mainClass>
<outputDirectory>${project.basedir}/docker</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
Then, build the image:
$ docker build -t spring-boot-app:latest .
Sending build context to Docker daemon 2.048kB
Step 1/3 : FROM openjdk:8-jre-alpine
8-jre-alpine: Pulling from library/openjdk
ff3a5c916c92: Pull complete
a8906544047d: Pull complete
a790ae7377b0: Pull complete
Digest: sha256:795d1c079217bdcbff740874b78ddea80d5df858b3999951a33871ee61de15ce
Status: Downloaded newer image for openjdk:8-jre-alpine
---> c529fb7782f9
Step 2/3 : COPY target/spring-boot-app-0.0.1-SNAPSHOT.war /app.war
---> d19bfa9fdfa7
Step 3/3 : CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", "/app.war"]
---> Running in 04bf7e97079a
Removing intermediate container 04bf7e97079a
---> 04872b157605
Successfully built 04872b157605
Successfully tagged spring-boot-app:latest
I'll cover the contents of the file and what happens during the build after this initial image is running.
Finally, you can take a look at the build results:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-boot-app latest 04872b157605 28 seconds ago 128MB
openjdk 8-jre-alpine c529fb7782f9 3 days ago 82MB
Docker image ls
lists the images on our system. Your new image is there, named spring-boot-app as specified in the build command. You will also see openjdk, which docker created as part of the build process.
Running a Docker Container
Now run the application in a container:
$ docker run -d -p 8080:8080 spring-boot-app:latest
e9418201d112b15b94a489ab9995d966807a2171e844810567b110f2b9be01ec
Point a web browser at http://localhost:8081/springbootapp/employees and you see the employee record.
Take a look at what's running:
$ docker ps
IMAGE STATUS PORTS NAMES
spring-boot-app:latest Up 2 minutes 0.0.0.0:8080->8080/tcp eloquent_varaham
Docker ps
displays the running containers on the host machine.
We see the container is up and running! You have a Spring Boot application running on Docker.
The command line to run this container had three arguments:
-d
- run as a daemon process and detach from the console.-p
- map port 8080 on the host machine to port 8080 in the container.- spring-boot-app:latest name:tag of the image to run.
Docker Images and Containers
If you look again at the headings above, you see you built an image and then ran a container. This terminology is important.
Containers
Simply put, Docker runs the applications in a container. It's important to note that these containers don't run in and are not virtual machines. They run on Linux and share the host system's kernel with each other. Implementations on non-Linux platforms such as macOS and Windows 10 use a Linux virtual machine for the Docker runtime.
Inside containers, applications are isolated from one another and the underlying infrastructure. Each container has a virtual filesystem and appears to have its own kernel. This simplifies application packaging and problems with an application are isolated to a container, protecting the rest of the machine.
Images
Images contain everything needed to run the container. "Everything" includes not just the code and libraries for the application, but also the operating system too.
Let's look at our Dockerfile again:
# Alpine Linux with OpenJDK JRE
FROM openjdk:8-jre-alpine
# copy fat WAR
COPY spring-boot-app-0.0.1-SNAPSHOT.war /app.war
# runs application
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", "/app.war"]
The first line tells docker where to start building; FROM openjdk:8-jre-alpine. This is the name of an existing image that provides the OpenJDK JRE on Alpine Linux. Alpine Linux delivers a lightweight, secure, and fast container for running Java applications.
The next line copies the web jar to the root of the image filesystem. A Dockerfile can include several COPY directives, and it can be used to copy entire file trees.
The last line is the command that will be executed to start our container. CMD
accepts an array of strings that make up the command line, similar to Runtime.exec.
When you built this image we saw this in the build output:
Status: Downloaded newer image for openjdk:8-jre-alpine
Docker retrieved that image as part of the build, and then it applied the rest of the file to that image.
You can view the list of steps that have been taken to build any image:
$ docker history spring-boot-app:latest
IMAGE CREATED BY SIZE
fb9139a8c8b8 /bin/sh -c #(nop) CMD ["/usr/bin/java" "-ja... 0B
d19bfa9fdfa7 /bin/sh -c #(nop) COPY file:f4a155b9ed7a8924... 46.2MB
c529fb7782f9 /bin/sh -c set -x && apk add --no-cache o... 77.8MB
<missing> /bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8... 0B
<missing> /bin/sh -c #(nop) ENV JAVA_VERSION=8u151 0B
<missing> /bin/sh -c #(nop) ENV PATH=/usr/local/sbin:... 0B
<missing> /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv... 0B
<missing> /bin/sh -c { echo '#!/bin/sh'; echo 'set... 87B
<missing> /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> /bin/sh -c #(nop) CMD ["/bin/sh"] 0B
<missing> /bin/sh -c #(nop) ADD file:093f0723fa46f6cdb... 4.15MB
This output is a list of images. The last eight are "missing" their image ids because they are not present on your system.
The top three, however, do have ids. The "created by" columns for these images displayed what step in our build created them:
CMD
- the directive from our Dockerfile.COPY
- copying our jar to the image.apk
- the Alpine Linux package tool installing the openjre package.
Running docker history -no-trunc spring-boot-app:latest
provides the complete commands. The output is too wide to display here.
Take a look at docker image ls
again:
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-boot-app latest fb9139a8c8b8 12 hours ago 128MB
openjdk 8-jre-alpine c529fb7782f9 4 days ago 82MB
There are two images: both yours and openjdk. Their ids match the ids of the first and third images in our history. The second image was an intermediate stage and doesn't exist.
Openjdk is still present on your system, while spring-boot-app exists as a set of diffs to the base image. Docker manages images and containers as layers, which conserves memory and disk space.
That's all for Part 1. Tune in tomorrow when we'll discuss managing Docker applications, building better images, and more!
Published at DZone with permission of Eugen Paraschiv, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments