Stackify is now BMC. Read theBlog

A Start to Finish Guide to Docker with Java

By: Eugen
  |  March 11, 2024
A Start to Finish Guide to Docker with Java

Intro 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 toward 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:

You can run the application with this command line

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 our 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 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.

Managing Docker applications

Starting and stopping Docker containers

Docker ps shows us what is running:

$ docker ps
IMAGE                       STATUS              PORTS                    NAMES
spring-boot-app:latest      Up 2 minutes        0.0.0.0:8080->8080/tcp   eloquent_varaham

The application is still up. It’s running in a container named eloquent_varaham.

We use the container name to control it. Let’s stop is:

$ docker stop eloquent_varaham
eloquent_varaham

When you check docker ps, it’s gone:

$ docker ps
CONTAINER ID        IMAGE               COMMAND

And if you try to open the index page again, it fails.

But the container is still there. Let’s look again:

$ docker ps -a
IMAGE                    STATUS                       PORTS          NAMES
spring-boot-app:latest   Exited (143) 2 minutes ago                  eloquent_varaham

Adding -a shows us stopped containers.

We can restart an existing container instead of creating a new one:

$ docker restart eloquent_varaham
eloquent_varaham
$ docker ps -a
IMAGE                    STATUS              PORTS                    NAMES
spring-boot-app:latest   Up 22 seconds       0.0.0.0:8080->8080/tcp   eloquent_varaham

The container is running again and you can open access the web app.

But you can stop and remove and container too:

$ docker stop eloquent_varaham
eloquent_varaham
$ docker rm eloquent_varaham
eloquent_varaham
$ docker ps -a
CONTAINER ID        IMAGE              PORTS               NAMES

Docker rm removes a container, but it must be stopped first.

Looking inside containers

Start a new container. Not the different port mapping argument:

$ docker run -d  -p 8081:8080 spring-boot-app:latest
69e430267b4347a6d9cbfc5b7fcce368187622c219105e8f3634b9c8116bb20b
$ docker ps
IMAGE                    COMMAND                  PORTS                    NAMES
spring-boot-app:latest   "/usr/bin/java -jar …"   0.0.0.0:8080->8080/tcp   sad_swartz

It’s running again, in a container named sad_swartz. Point your browser at port 8081 to load the page. You can remap container ports to different host ports on the command line.

Now take a look at the container logs:

$ docker logs sad_swartz

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.1.RELEASE)

2018-06-10 02:36:53.032  INFO 1 --- [           main] c.s.Application        
   : Starting Application
...

Docker logs [container name] displays the container’s output.

You can also attach a shell to our container:

$ docker exec -it sad_swartz sh
/ # ps
PID   USER     TIME   COMMAND
    1 root       0:52 /usr/bin/java -jar -Dspring.profiles.active=default /app.war
   46 root       0:00 sh
   50 root       0:00 ps
/ # ls
app.war  dev      home     media    proc     run      srv      tmp      var
bin      etc      lib      mnt      root     sbin     sys      usr
/ #

Docker’s exec option executes a program inside a container. Since the Alpine image is minimalist, you need to use sh. You can only execute commands that are already in the image.

The -it flags allow us to interact with the shell.

Building a better image

Sharing directories

All state, including the filesystem, lives only for the lifetime of the container. When you rm the container, you destroy the state also.

If you want to preserve data, it needs to be stored outside of the container. Let’s demonstrate this by mapping the application log to a directory on the host system.

First, add a logback configuration to the application:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>/var/log/Application/application.log</file>
        <append>true</append>
        <encoder>
            <pattern>%-7d{yyyy-MM-dd HH:mm:ss:SSS} %m%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="FILE" />
    </root>
</configuration>

Then modify your Dockerfile to use it:

FROM openjdk:8-jre-alpine

COPY spring-boot-app-0.0.1-SNAPSHOT.war /app.war

COPY logback.xml /logback.xml

CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", 
  "-Dlogging.config=/logback.xml", "/app.war"]

You’re copying the logback.xml into the image, and adding the logging configuration to the command line.

The logging configuration places the application logs in /var/log/Application/.

Rebuild the image:

$ docker build -t spring-boot-app:latest .
Sending build context to Docker daemon  131.1MB
Step 1/4 : FROM openjdk:8-jre-alpine
 ---> c529fb7782f9
Step 2/4 : COPY target/spring-boot-app-0.0.1-SNAPSHOT.war /app.war
 ---> Using cache
 ---> d19bfa9fdfa7
Step 3/4 : COPY src/main/resources/logback.xml /logback.xml
 ---> Using cache
 ---> d62f97d9900d
Step 4/4 : CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", 
  "-Dlogging.config=/logback.xml", "/app.war"]
 ---> Using cache
 ---> fb9139a8c8b8
Successfully built fb9139a8c8b8
Successfully tagged spring-boot-app:latest

Docker didn’t download the openjdk:8-jre-alpine image since docker has it cached locally.

Look at the build command. You specify an image tag with -t. This is the same tag that you pass to docker run. You provide the working directory last.

Now, you need to map the directory to a directory on the host when you run our container:

$ docker run -d -v /var/log/app:/var/log/Application/ 
  -p 8080:8080 spring-boot-app:latest

The -v option maps /var/log/app on our host system to /var/log/Application/ in the container.

When you run this command, you can see a log file created in the mapped directory.

Naming Docker containers

You’ve been letting docker assign names to your containers. You can override this with –name:

$ docker run -d --name bootapp -v /var/log/app:/var/log/Application/ 
  -p 8080:8080 spring-boot-app:latest
57eb3f1998f503dc146d1f3b7ab9a6b221a939537be17ffc40fd410e2b72eda3
$ docker ps
IMAGE                    STATUS         PORTS                    NAMES
spring-boot-app:latest   Up 2 seconds   0.0.0.0:8080->8080/tcp   bootapp

Adding packages

When you looked at your image’s history, you saw the command for adding the jre to Alpine. You can add packages to Alpine in your Dockerfile, too. Let’s add bash to the container.

First, add the APK command to our Dockerfile:

# Alpine Linux with OpenJDK JRE
FROM openjdk:8-jre-alpine
RUN apk add --no-cache bash

# Copy WAR
COPY spring-boot-app-0.0.1-SNAPSHOT.war /app.war

# copy fat WAR
COPY logback.xml /logback.xml

# runs application
CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", 
  "-Dlogging.config=/logback.xml", "/app.war"]

Then build the image with the same directives as before:

$ docker build -t spring-boot-app:latest .
Sending build context to Docker daemon     40MB
Step 1/5 : FROM openjdk:8-jre-alpine
 ---> c529fb7782f9
Step 2/5 : RUN apk add --no-cache bash
 ---> Using cache
 ---> 3b0c475c9bd0
Step 3/5 : COPY spring-boot-ops.war /app.war
 ---> c03bd6c6ace5
Step 4/5 : COPY logback.xml /logback.xml
 ---> b2f899ebec17
Step 5/5 : CMD ["/usr/bin/java", "-jar", "-Dspring.profiles.active=default", 
  "-Dlogging.config=/logback.xml", "/app.war"]
 ---> Running in 3df30746d7a8
Removing intermediate container 3df30746d7a8
 ---> cbbfb596a092
Successfully built cbbfb596a092
Successfully tagged spring-boot-app:latest

The output is a little different this time. You can see where bash was installed in step two.

Finally, after you run the container, you can shell in with bash:

$ docker exec -it bootapp bash
bash-4.4# ls
app.war      etc          logback.xml  proc         sbin         tmp
bin          home         media        root         srv          usr
dev          lib          mnt          run          sys          var
bash-4.4#

Passing command line variables

So far, you’ve been running the Spring Boot application with the active profile set to default. You may want to build a single jar with different profiles and then select the correct one at runtime. Let’s modify our image to accept the active profile as a command line argument.

First, create a shell script in the docker directory that runs the web application:

#!/bin/sh

java -Dspring.profiles.active=$1 -Dlogging.config=/logback.xml -jar /app.war

This script accepts a single argument and uses it as the name of the active profile.

Then, modify your Dockerfile to use this script to run the application:

# Alpine Linux with OpenJDK JRE
FROM openjdk:8-jre-alpine
RUN apk add --no-cache bash

# copy fat WAR
COPY spring-boot-app-1.0.0-SNAPSHOT.war /app.war

# copy fat WAR
COPY logback.xml /logback.xml

COPY run.sh /run.sh

ENTRYPOINT ["/run.sh"]

Dockerfile offers two mechanisms for starting a container; the ENTRYPOINT and the CMD. Simply put, the ENTRYPOINT is the program that is executed to start the container and CMD is the argument passed to that program.

The default ENTRYPOINT is /bin/sh -c. Until now, you were passing our Java command array to a shell.

Now, the dockerfile is copying the script to the image and then defining as the image’s ENTRYPOINT. There is no CMD.

Build this image and then run it with dev as the final argument on the command line:

$ docker run -d --name bootapp -v /var/log/app:/var/log/Application/ 
  -p 8080:8080 spring-boot-app:latest dev

And then take a look at the logs for the active profile:

$ grep profiles /var/log/webapp/application.log
2018-06-11 00:33:50:016 The following profiles are active: dev

You can see that the profile setting was passed into the JVM.

Publishing images

We’ve only used the image on your development system. Eventually, you’ll want to distribute it to clients or production systems. This is done with a registry, where images are pushed with a name and tag and then pulled when they are run as containers. You saw this in action at the start of this tutorial when docker pulled the hello-world image for you.

The first step is to create an account on Docker Cloud. Go and create an account there if you don’t already have one.

Next, log in to the Docker registry on our development system:

$ docker login
Username: baeldung
Password:
Login Succeeded

Next, tag the image. The format for tags is username/repository:tag. Tags and repository names are effectively freeform.

Tag the image and then list the images on your system to see the tag:

$ docker tag spring-boot-app baeldung/spring-boot-app:.0.0.1
$ docker image ls
REPOSITORY                         TAG               IMAGE ID        CREATED             SIZE
spring-boot-app                    latest            f20d5002c78e    24 minutes ago      132MB
baeldung/spring-boot-app   1.00              f20d5002c78e    24 minutes ago      132MB
openjdk                            8-jre-alpine      c529fb7782f9    4 days ago          82MB

Note that the new image tag and the original image have the same image ID and size. Tags don’t create new copies of images. They’re pointers.

Now you can push the image to Docker Hub:

$ docker push baeldung/spring-boot-app:.0.0.1
The push refers to repository [docker.io/baeldung/spring-boot-app]
8bfb0f145ab3: Pushed
2e0170d39ba4: Pushed
789b0cedce1e: Pushed
f58f29c8ecaa: Pushed
cabb207275ad: Mounted from library/openjdk
a8cc3712c14a: Mounted from library/openjdk
cd7100a72410: Mounted from library/openjdk
1.00: digest: sha256:4c00fe46080f1e94d6de90717f1086f03cea06f7984cb8d6ea5dbc525e3ecf27 size: 1784

Docker push accepts a tag name and pushes it to the default repository, which is Docker Hub.

Now, if you visit your account area on hub.docker.com, you can see the new repository, the image, and the tag.

Now you can pull the image down and run it on any system:

$ docker run -d --name bootapp -v /var/log/app:/var/log/Application/ 
  -p 8080:8080 ericgoebelbecker/spring-boot-app:.0.0.1 dev 
Unable to find image 'baeldung/spring-boot-ops:1.00' locally
1.00: Pulling from baeldung/spring-boot-ops
b0568b191983: Pull complete
55a7da9473ae: Pull complete
422d2e7f1272: Pull complete
3292695f8261: Pull complete
Digest: sha256:4c00fe46080f1e94d6de90717f1086f03cea06f7984cb8d6ea5dbc525e3ecf27 
Status: Downloaded newer image for baeldung/spring-boot-app:.0.0.1

This is the output of run on a different system from the one I built on. Similar to the way you ran hello-world, you passed the image tag to docker run. And since the image was not available locally, Docker pulled it from Docker Hub, assembled it, and ran it.

Conclusion

Docker is a robust platform for building, managing, and running containerized applications. In this tutorial, we installed the tools, packaged a Spring Boot application, looked at how we can manage containers and images, and then added some improvements to our application.

Finally, we published our image to Docker Hub, where it can be downloaded and run on any Docker-enabled host.

Now that you understand the basics keep experimenting and see how you can use Docker to package and distribute your Java applications.

As always, the source code for this tutorial is available over on Github.

Stackify’s Application Peformance Management tool, Retrace keeps Java applications running smoothly with APM, server health metrics, and error log integration.  Download your free two week trial today!

Improve Your Code with Retrace APM

Stackify's APM tools are used by thousands of .NET, Java, PHP, Node.js, Python, & Ruby developers all over the world.
Explore Retrace's product features to learn more.

Learn More

Want to contribute to the Stackify blog?

If you would like to be a guest contributor to the Stackify blog please reach out to [email protected]