Hello, dear habravchane! Today I would like to tell you how to feed a Java application to a docker, how to do it better, and what not to do. I have been working on Java for over 10 years, and spent the last three years in close contact with Docker, so I have a certain idea that it can and cannot. But hypotheses need to be tested in practice, aren’t they?
I presented the whole process as a good old computer game with a warm tube pixel art.
We begin, as befits any game, with a briefing. As an introduction, take a little docker advertising.
')
On the
docker's website, you can familiarize yourself with a number of advertising promises - namely, with the promise to increase the speed of development and deployment as much as
13 times and increase portability in development (in particular, get rid of the sacramental “works on my machine”). But does this correspond to reality?
Now we will try to prove / disprove these statements.
Level 1
Since we are in the game, we will begin, as it should be, from the simplest level.
What is our mission on the first level? Probably, for many, this is something very trivial and understandable: we must “wrap” the most primitive Java application in Docker.

To do this, we need a simple Java class that displays the sacramental
Hello JavaMeetup! Also, in order to create a docker image, we need a
Dockerfile . Syntax is extremely simple - use
java: 8 as the base image, add our Java class (
ADD command), compile it (using the
RUN command) and specify the command that will run when the container is started (
CMD command).
HelloWorld.java
public class HelloWorld { public static void main (String[] args) { System.out.println("Hello World!"); } }
Dockerfile FROM java:8 ADD HelloWorld.java . RUN javac HelloWorld.java CMD ["java", "HelloWorld"]
Docker commands: $ docker build -t java-app:demo . $ docker images $ docker run java-app:demo
In order to build this whole business, we need, in fact, one team - this is
docker build . When building, we specify the name of our image and the tag that we assign to it (this way we will be able to version various assemblies of our application). Next, make sure that we have compiled the image by running the
docker images command . In order to run our application,
run the docker run command .
Hooray, everything went fine, and we are great ... Or not?

Yes, we have completed the mission. But we have to score points for anything. For what, you ask, and how to avoid such blunders next time?
- The basic image of the docker that we used is declared deprecated and is not supported by the docker community . Even at DockerCon17, many of the Java EE world friends Arun Gupta recommended using openjdk as a base image (which is also hinted at by the description and update dates of the images https://hub.docker.com/_/openjdk/ ).
- To reduce the size, it is better to use images based on Alpine - images based on this distribution are the most lightweight.
- We compile using the jdk image, run it using jre (save disk space, we still need it).
Now, we can assume that the first level has been completed successfully. We rise to the second.
Useful link for passing the first levelLevel 2

When dealing with Java, we will most likely use Maven or Gradle. Therefore, it would be convenient to somehow integrate our build systems with Docker in order to have a unified environment for building the project and docker images.
Fortunately for us, most plugins are already written — for both Maven and Gradle.

The most popular Maven plugins for Docker
fabric8io and
spotify . For Gradle, we can use the plugin Benjamin Mushko - one of the developers of Gradle and the author of the book "Gradle in Action".
To connect the docker to the application's build system, in the gradle configuration it is enough to create several tasks that will collect and run our containers, as well as indicate some general information from the discharge — which image to use as the baseline and what name to give to the assembled image.
Let's not be verbose: take the
bmuschko / gradle-docker-plugin and Gradle
plugin (Maven fans and XML fans, wait a bit!).
Perform our first task, but now with the help of this plugin. The main parts of build.gradle that we need:
docker { javaApplication { baseImage = 'openjdk:latest' tag = 'java-app:gradle' } } task createContainer(type: DockerCreateContainer) { dependsOn dockerBuildImage targetImageId { dockerBuildImage.getImageId() } } task startContainer(type: DockerStartContainer) { dependsOn createContainer targetContainerId { createContainer.getContainerId() } }
We start the
gradle startContainer command and see the build of our image and even the launch of the container. But instead of the desired message “Hello JavaMeetup!” We receive a notification of a successful build!

Did we make a mistake somewhere? Not really, just need to redirect the output of our container to the console:
task logContainer(type: DockerLogsContainer, dependsOn: startContainer) { targetContainerId { startContainer.getContainerId() } follow = true tailAll = true onNext { message -> logger.quiet message.toString() } }
Run the command
gradle logContainer and ... Hurray, the cherished message and the level passed.

Here, strictly speaking, that's all. We don’t even need a Dockerfile (but it won’t be superfluous - you never know, Gradle will not be at hand).
Moving on!
Level 3

Most likely, in real life, our application will do something more cunning than displaying the Hello World screen. Therefore, at the next level, we will learn how to run a complex application - the Spring web application, which will bring us some records from the database.
In order to raise the base and the application itself, we will use the
Docker Compose . To begin with, we will create a new file (the next new configuration file, will you breathe, but won't it stop us?) -
docker-compose.yml . In it, we simply prescribe services to raise the image of the base and the image of the application. Docker Compose will find the yml-file in the current directory and pick up or collect the containers and images we need.

In order for the whole business to start, we will pre-assemble the image. In
this example , the maven plug-in for Docker (hooray, XML!) From fabric8io is used - so, first, run the
mvn install command:
<plugin> <groupId>io.fabric8</groupId> <artifactId>docker-maven-plugin</artifactId> <version>0.20.1</version> <configuration> <images> <image> <name>app</name> <build> <dockerFileDir>${project.basedir}/src/main/docker</dockerFileDir> <assembly> <mode>dir</mode> <targetDir>/app</targetDir> <descriptor>${project.basedir}/src/main/docker/assembly.xml</descriptor> </assembly> </build> </image> </images> </configuration> <executions> <execution> <id>build</id> <phase>install</phase> <goals> <goal>build</goal> </goals> </execution> </executions> </plugin>
Let's wait until our project and image of the docker get together, move to the directory with the yml file and run the command
docker-compose up -d .
Verify that both our containers are started by running the
docker ps command .
In order to make sure that our web application works and gets something from the database, we can directly change something in the database, then go to
http: // localhost: 8080 / and see the desired data.
All this may seem complicated, but in reality it is extremely simple. The third level is passed. Almost.
We still have a bonus level. On it, we will play a little (quite a bit) in Docker Swarm - or to be exact, in
Docker Swarm Mode .
Bonus Level

Docker Swarm Mode is not particularly complicated - it is just a cluster of machines on which Docker stands. For a user, this cluster looks like one machine, and all commands work almost as if the Docker Swarm did not exist.
In swarm mode, you can run multiple instances of our application - for load balancing, for example. Also here appears such an abstraction as a stack: with the help of Docker Swarm, we can deploy a whole bunch of applications as a whole. And, like normal scaling, we can deploy multiple
stack replicas.
Docker commands in swarm mode:
$ docker service create --name japp --publish 8080:80 --replicas 3 java-app:demo $ docker stack deploy -c docker-compose.yml javahelloworld
In essence, the command syntax is extremely simple and resembles the creation of ordinary containers.
We can also use docker compose:
$ docker-compose scale jm-app=3
Well, for the last three levels, we seem to have achieved the portability of java applications. It is time to go to the last level and try to confirm or deny the statement that Docker makes the phrase “works on my machine” no longer relevant.
Final level

Imagine that we have a heavy application. Or a large number of microservices that can be on the same machine. In this case, the Java application (to be exact, the JVM) will surely grapple with our little blue whale in the fight for the resources of the host machine. By the way, this is well described in
this article .

At this level, there will be the least number of code examples, but there will be different configurations of launching the docker container. The main tools for isolating processes and resources used by Docker are
cgroups and
namespaces . But the main problem lies in the fact that Java is all on the drum a little. We have a voracious and a bit greedy, sees that resources are actually more, even if we set memory limits when creating a container with a java-application using the - memory flag. This can be seen simply by running the free command inside the container. From here follows a rather general recommendation for Java 8 to set the –Xmx parameter, and the –memory parameter to be at least twice as large as –Xmx. Good news from the Java 9 fields - they added cgroups support.

It is quite simple to simulate a memory leak in Java. We’ll just take the
valentinomiazzo / jvm-memory-test image that’s already completed and run it with different heap size options and - memory for the docker.
In the first case, the container has been given out less than the java application, and we get a vague error. And I would like to get OutOfMemoryException. If you inspect the "dead" container, you can see that it was killed by
OOMKiller , and this can lead to unpredictable consequences, java-process hang, incorrect closing of resources and all other offensive things (I have even met kernel-panic). Not the most pleasant thing that can happen.
Raise the stakes, give more memory to the container. This time, we can catch the OutOfMemoryException and, after inspecting, make sure that our container did not touch OOMKiller, and we are spared from all the above mentioned troubles.
The last level is passed, let's try to summarize.
Resume
So, what did we get as a result, having passed all the levels of our game? What about Docker promises to move us mountains?
Portability is not as good as we would like, but Java 9 seems to promise to solve these problems. With increasing flexibility, everything is more pleasant: with the docker, we get a reproducible environment configuration in the code, and not far from the main code. It’s easier to keep track of such things, rather than who, what and when you corrected, replaced or spoiled something under the root. On the whole, it is possible to achieve a good reduction in resources due to the possibility of running multiple containers on one machine — this can be critical when testing.
That is, I would say that the docker is ideal for testing and / or development. But when working in production you need to be careful, because the load in this case can be much higher. And getting a docker’s fault is really quite unpleasant.
And finally - the very flags that are needed in order to make Java 9 friends with the docker!

Game over
Useful links for passing the last level:https://hackernoon.com/crafting-perfect-Java-docker-build-flow-740f71638d63https://jaxenter.com/nobody-puts-Java-container-139373.htmlhttps://github.com/valentinomiazzo/docker-jvm-memory-testPS All the examples mentioned above can be found here:
github.com/alexff91/Java-meetup-2018