A Hitchhiker's Tour to Containerizing a Java application
Frankel, Nicolas
While a plain Dockerfile gets the job done, there are actually many more ways to containerize your Java app. They come with a couple of pros, and some cons. As “the Cloud” becomes more and more widespread, now is a good time to assess how you can containerize your Java application. I assume you’re able to write a a Dockerfile around the generated JAR. However, each time the application’s code will change, the whole image will need to be rebuilt. If you’re deploying to a local Kubernetes cluster environment, this increases that much the length of the feedback loop. In this demo-based talk, I’ll present different ways to get your Java app in a container: Dockerfile, Jib, and Cloud Native Buildpacks. We will also have a look at what kind of Docker image they generate, how they layer the images, whether those images are compatible with skaffold, etc.
 Hi folks, welcome to this talk about containerizing Java applications. I'm Nicola Frantl, I've been a former developer, architect, whatever, and right now I work as a developer advocate. Since the trend about containerization started, I became very interested in Docker and Kubernetes and all the surrounding technologies. I work for a company called Hazelcast, and Hazelcast has two products. The main one is an in-memory data grid, and you can think about an in-memory data grid as distributed data data structures. So you can short your data and replicate your data over different nodes over the network. The second one is Hazelcast chat, and it's an in-memory stream processing engine, so it leverages the distributed nature of Hazelcast's time-dg to do this stuff. Today I won't talk about Hazelcasts, I will just use it, and I will talk about how we can containerize our Java app. So let's start with a non-Java app, with a Python application. And how do you containerize a Python application? Well, it's quite straightforward, actually. You iterate from Python image, you add your file that contains the dependencies, you install them, you add the script itself, and then you run the scripts, and it's done. LikeWIFE for a Node.js application, you iterate from your nodes, and there's image, you copy the file that contains the dependencies, you install the dependencies, and you run again the script. And all of those scripting languages have in common is that, well, there is no intermediate bytecode, there is no compile phase. So the main gist is to iterate from the right image, to iterate the dependencies, and to run the script. It's wussy-weir, what you see is what you run. Whereas in Java, it's slightly more complicated, because we have several layers. Well, the first one is actually the JAR that might contain all the JAR, depending on how you want to run it. And here I'm using HazelCost as a dependency, but again, it's not warranted. Then you have your jivm, and then you have your Docker. And in my sample app, I have a REST endpoint, I want to expose the REST endpoint to the outside world, so I can put and get data in HazelCost. So how do we do it? Well the easy way is we create the JAR outside of Docker, so we just mvn a clean package, and then we have a Docker file that wraps it. Let's do it. So here I have my application. It's a string boot application, because in the end, I will show you how you can use it with string boot, but it has only to dependency the web for the REST endpoint and HazelCosts. And then nothing mind-blowing. It's just very straightforward, and the Docker file is just, hey, just copy the JAR file and put it in Docker, and then run java.dashjar the Docker JAR. So here I will be caging the application and the n clean package. Then I will build the Docker from with the Docker file, and because I'm lazy, I will just do that. I won't do it every time, because in the end, in some alternatives, it can take quite some time. Here it's pretty straightforward, but later options can be quite time-consuming. And now I can Docker run dash dash or M, because I don't want to keep it in the end, and P, because I want to be to expose the ports to the outside world, and it's called string in Docker column five. So the application starts. I have HazelCosts running in the background, and now I can check and I will put some data first, exposed HTTP, localhosts, 8080, let me say world, it's put, because I don't know my own application. Now it works, and then I can curl, edge, to check that the data is still there, 8080, and yes, it's here, I can put another stuff and say hello, Nikola. And yes, so I have my data. So that's the straightforward things to do, and it works. But the issue here is that we actually need to build first jar outside of Docker, and then we use Docker to package the jar, which is not super straightforward. So the next step is to run the entire build inside of Docker. So here I have my updated Docker file, and to run the build inside of Docker, we actually needs to have a compiler. So we inherit from a GDK and not a simple GRE, and we do our like, black magic, we copy the MVN folder, because we are supposed to be independent, and we run the package, skip the test, because of course there are no tests, but anyway, and it works pretty well. And now if we do the same, so I will get back to the terminal, I will stop this one, I will run 1.0, because I already like created it before. And now I can check that it still works. So it's the data that is still there, everything works as expected. And yes, it works, but there are several downsides, and the biggest downsides is we embed a GDK in the final image, and it has two, like, big issues. The first is an increased size, and we want our Docker image to be as small as possible, when possible, and the biggest issue is actually the fact that it can compile Java code, so you will actually deliver in production an image that can compile code, and it's a huge security issue, you never want to do that. The second problem is about version handling. As you noticed, I need myself to set the version when I build the Docker image, so I need to remember that this version of the POM is the same as the version of the Docker build, I need to keep them both in sync at the same time. And finally, there is no layering, so what do I mean by that? You know that Docker images are layered, so you inherit from the base image, and you add some more stuff, which creates another image, this image itself can be a parent image, and so on and so forth. And the idea is that you can keep the previous images if they were not changed, if they were not touched. Docker doesn't need to rebuild them anymore. And now we have this drawer, and this drawer, as soon as you change a library, as soon as you change a class file, as soon as you change anything, you need to rebuild the whole Docker image. So, how can we see that? Well there is a nice stuff called dive. So what we will do is we will dive into the image, so I will stop it here, and I will dive into sprint in Docker 1.0. Takes a bit of time, of course. And here, this is how it looks. So here I inherit from this base image, you can see there was some installation going on, and here I was not the one to copy several things, and here this is what I did, this is what I added to my image, this is this mvn package where I skipped the tests. All the rest is in the returns from the previous image. And here, this is the issue that this mvnw package adds the whole stuff on the other side, and as soon as I change anything, well it will rebuild the whole image, including fetching the dependencies. As I mentioned, you need to redo everything, and parts of the build is to fetch the dependencies. And it's not great fetching the dependencies, as you know that Maven, but every build tool that relies on all the jobs, they don't load the whole internet, and through alternative dependencies here, you can see that there are a lot here. So every transitive dependency is here, and we don't want that. So how can we do better? One way to do better is actually to do multi-stage builds. And multi-stage builds allows you to, inside the Sandocker file, to change the base image. So you can copy a file from a previous stage into your current stage. This is pretty widespread, it's really a great thing to use, but if you are using Skaffold, Skaffold is the tool that lets you configure your Docker to automatically publish the Kubernetes, either locally or micremote, probably locally. So the problem of multi-stage builds is they are not compatible with Skaffold, so be careful about that. Otherwise, this is how it looks like. I will need to quit that. Please let me quit. Yes. And I will just use the multi-layer here. So now I have two different layers. I have two different stages. Sorry about the semantics. The first stage is about building and I'm using a JDK. And once I get the drawer, I use another stage where I use single JRE and I copy the drawer from the previous one into this image. So this ID here and it's 1.1 and if we dive into it, as you can see, here I have like many, many less layers and more importantly, here I have one single drawer. I don't have the dependencies issue that I had before, which is a bit better. Still I have this drawer layer and the problem of this drawer layer, again, if I change a class file, which will happen all the time, I will need to rebuild everything. I don't need to fetch the dependencies, but I need to rebuild the drawer. So the next ID is to start thinking about the drawer as a distribution unit, but actually your Docker image is also a distribution unit and they are pretty redundant. Without Docker, JAR is the best distribution unit you can get, but with Docker images, they are useless because running a JAR inside Docker image, why don't we just run the JAR exploded? And that's the train of thoughts of the people behind the JIP plugin, which is a standard of Mavin plugin. And with JIP, you just like configure your Mavin PON to use JIP and it also works with Gradle and it can either push to a remote Docker repository or to the local Docker. You can add a lot of configuration options, including choosing the parent image and it runs the exploded JAR and there are a lot of benefits. Well the first one is there is no Docker file because probably all your Java Docker files will look the same, so it's no use repeating them over and over. The second benefit is you have an automated version and from the POM. So if your POM is version X, then JIP knows that it will create version X of the Docker image. And then there are different layers. So there are dependencies and resources and compile code, meaning that the compile code is the stuff that changes the most. And there is no issue, well there is not, it's not useful to every time get the dependency again or copy the resources again if only your code changes. So this layered way of doing things is actually the best because if you change the compile code, the code, you compile the code, you only change the utmost layer. Depending on how you look at things, the one which is at the bottom or at the top, whether your parent is at the top or at the bottom. So that's pretty smart and actually in the latest version, you have four layers, one is about snapshot dependencies because snapshot dependencies are supposed to change more often than simple dependencies. And so it handles that for you. So let's do that and let's see that here in this, here I've removed the Docker file entirely, there is no Docker file anymore, but I've added the JIP-Maven plugin. And here I'm telling it that it will create the image and as you can see it syncs the image version with the project version and the layers look actually very nice. So I need to remember its version 2.0, I will dive into it. And here you can see that here you've got the parent stuff and here you've got the JIP-Maven plugin, the first one is about the libraries, the second one is about the snapshot libraries, again because they change more often. The third one is about the resources, so I have the files and the final one is about the compiler code. So if I need, just need to change the compiler code, all the previous layers will be kept and the building of the image will be much, much faster. Also with JIP you can configure and for example to change the parent image. So here I will be using an Alpine gearing to keep the size of my final image smaller and if I do a Docker image is grab, spring in Docker. We can see here that I have Russian 2.0 and 2.1 and the 2.1 uses the Alpine parent image. So you can see I have a few megabytes which can be a good thing if you've got a small image you want to put them very small. The next option if you've got a spring boot application is you skip the JIP plugin entirely and use the spring boot mechanism. And spring boot is also able to create a Docker image that is nicely layered by default to have like the same ones as in JIP, the dependencies, the resource, the snapshot dependencies and the compile code, different order though, but you can customize them. So if you know that some parts of your applications are going to change much more often than others, you can have a dedicated file, an index file that tells you, hey, this package or this stuff you will put it into this layer and this other into this layer. In order to do that, however, we need to get back to a Docker file. So here is how you do it with the spring. And I have a Docker file again and it's like a multi-stage build. You can see here you have three stages. The first one creates the package, creates the JAR. Then you have a second one that actually explodes the JAR and the third one will actually run the application through like a specific spring boot mechanism. This is a lot of work, but instead of doing that by ourselves, it would be much, much better to let somebody else do it. And that's the ID behind the BuildPacks. BuildPacks are backed by the Cloud Native Foundation and it's a tool that's able to understand how to build your project. So if you remember, if you have been doing cloud stuff since some years, you might know about Iroku. And when you were using Iroku, you would just like push, you would get push your sources to Iroku. And then Iroku on their own servers would try to sniff what kind of project it was and build it accordingly. So it would say, oh, I see a pump file. So it's probably a Maven project. So I will build it with Maven. And right now, this like way to do stuff was finally seemed to be worthwhile. And Iroku was joined by a team where PyVol told Tensu, I don't remember what their name is right now. They were joined by Spring. Now they both provide this like standard of BuildPacks. You've got a lot of configuration options. But you can pretty much do what you want with some limitation, of course. So the ID is that you've got no Docker file. It's scaffold friendly, and because I'm using the Springboots project, it knows it's a Springboot project. So it can use the previous four layers or if you customized it, all the layers. However, there are some downsides as well. Again, we get back to the fact that there is no sync between the pump and the Docker image. It's not that there is no choice of the parent image. It's a very limited choice. And if you want to change the parent image, you need to put an image that is compatible and I try to dive into it. It's just not some labels. It seems to be a pretty, pretty complex and dedicated process. I've noticed it takes a long time, even without changes. So perhaps I mismanage it, but for me, it's very long. It's especially very long because the build images, they don't move it every time. So every time it downloads the TDK again. So you can do it by running, first you need to install the build pack, then you run pack, you set the parent builder, sorry, and it will infer the build image. But why do that? With the latest version of Springboots, we can do the same inside of Maven or Gradle and it will use it under the cover. So how do we do it? It's quite easy. We just run, sorry, not here, here. We just run spring, mdm, always mdm. Springboots builds image. Again, the process is very, very long, so I won't show it to you. And in the end, this is what we get. We got rid of the Docker file again. And here we have the layer configuration enabled. And as you can see, nothing had changed otherwise. So the plugin itself knows how to create Docker images. That's pretty good. And the really, really nice stuff is that there are configuration options and it's very easy to change the configuration option to use Graal VM and to build a Graal VM native image. For example, here, that's the last stuff I want to show you, to create a native image that just adds an environment variable, the dp-boots-native-image-true, and that's done. And if we check again about the size of the images, so they are here. Here you have the Springboots inside a GVM. And here you have the Springboots inside a normal native process. So if you are interested in cloud stuff, if you want to have fast startup time and low memory consumption, then you can just use these kind of images. And it's a wrap. I just want to finish by mentioning the Docker squash. You can try to flatten the layers. In this simple application, I tried to flatten the layers from 1.0 and it was less than one person gain of the size. And from the 5.0, it was again less than one person gain of the size. So depending on what you value, if you value faster build time or smaller image sizes, you might want to check Docker squash. Might depend on your use case and probably on your parent images. But with the sample, my layers were nicely done so Docker squash doesn't bring any benefit. So as a recap, when you want to Dockerize your Java application, you must think about several things. You must think about the sync of the version between the pump and the image. You must think about organizing the layers to develop faster for your builds to be faster. And probably forget about squash though it might have seemed super popular at some point. It's not anymore. So thanks for listening to me. You can read my blog, you can follow me on Twitter and more importantly, if you want to try everything that I do here, everything is on Github. So thanks a lot and have a good day.
 Hi folks, welcome to this talk about containerizing Java applications.  I'm Nicola Frantl, I've been a former developer, architect, whatever, and right now I work  as a developer advocate.  Since the trend about containerization started, I became very interested in Docker and Kubernetes  and all the surrounding technologies.  I work for a company called Hazelcast, and Hazelcast has two products.  The main one is an in-memory data grid, and you can think about an in-memory data grid  as distributed data data structures.  So you can short your data and replicate your data over different nodes over the network.  The second one is Hazelcast chat, and it's an in-memory stream processing engine, so  it leverages the distributed nature of Hazelcast's time-dg to do this stuff.  Today I won't talk about Hazelcasts, I will just use it, and I will talk about how we  can containerize our Java app.  So let's start with a non-Java app, with a Python application.  And how do you containerize a Python application?  Well, it's quite straightforward, actually.  You iterate from Python image, you add your file that contains the dependencies, you install  them, you add the script itself, and then you run the scripts, and it's done.  LikeWIFE for a Node.js application, you iterate from your nodes, and there's image, you copy  the file that contains the dependencies, you install the dependencies, and you run again  the script.  And all of those scripting languages have in common is that, well, there is no intermediate  bytecode, there is no compile phase.  So the main gist is to iterate from the right image, to iterate the dependencies, and to  run the script.  It's wussy-weir, what you see is what you run.  Whereas in Java, it's slightly more complicated, because we have several layers.  Well, the first one is actually the JAR that might contain all the JAR, depending on how  you want to run it.  And here I'm using HazelCost as a dependency, but again, it's not warranted.  Then you have your jivm, and then you have your Docker.  And in my sample app, I have a REST endpoint, I want to expose the REST endpoint to the  outside world, so I can put and get data in HazelCost.  So how do we do it?  Well the easy way is we create the JAR outside of Docker, so we just mvn a clean package,  and then we have a Docker file that wraps it.  Let's do it.  So here I have my application.  It's a string boot application, because in the end, I will show you how you can use it  with string boot, but it has only to dependency the web for the REST endpoint and HazelCosts.  And then nothing mind-blowing.  It's just very straightforward, and the Docker file is just, hey, just copy the JAR file and  put it in Docker, and then run java.dashjar the Docker JAR.  So here I will be caging the application and the n clean package.  Then I will build the Docker from with the Docker file, and because I'm lazy, I will just  do that.  I won't do it every time, because in the end, in some alternatives, it can take quite some  time.  Here it's pretty straightforward, but later options can be quite time-consuming.  And now I can Docker run dash dash or M, because I don't want to keep it in the end, and P,  because I want to be to expose the ports to the outside world, and it's called string  in Docker column five.  So the application starts.  I have HazelCosts running in the background, and now I can check and I will put some data  first, exposed HTTP, localhosts, 8080, let me say world, it's put, because I don't know  my own application.  Now it works, and then I can curl, edge, to check that the data is still there, 8080,  and yes, it's here, I can put another stuff and say hello, Nikola.  And yes, so I have my data.  So that's the straightforward things to do, and it works.  But the issue here is that we actually need to build first jar outside of Docker, and then  we use Docker to package the jar, which is not super straightforward.  So the next step is to run the entire build inside of Docker.  So here I have my updated Docker file, and to run the build inside of Docker, we actually  needs to have a compiler.  So we inherit from a GDK and not a simple GRE, and we do our like, black magic, we copy  the MVN folder, because we are supposed to be independent, and we run the package, skip  the test, because of course there are no tests, but anyway, and it works pretty well.  And now if we do the same, so I will get back to the terminal, I will stop this one, I will  run 1.0, because I already like created it before.  And now I can check that it still works.  So it's the data that is still there, everything works as expected.  And yes, it works, but there are several downsides, and the biggest downsides is we embed a GDK  in the final image, and it has two, like, big issues.  The first is an increased size, and we want our Docker image to be as small as possible,  when possible, and the biggest issue is actually the fact that it can compile Java code, so  you will actually deliver in production an image that can compile code, and it's a huge  security issue, you never want to do that.  The second problem is about version handling.  As you noticed, I need myself to set the version when I build the Docker image, so I need to  remember that this version of the POM is the same as the version of the Docker build, I  need to keep them both in sync at the same time.  And finally, there is no layering, so what do I mean by that?  You know that Docker images are layered, so you inherit from the base image, and you add  some more stuff, which creates another image, this image itself can be a parent image, and  so on and so forth.  And the idea is that you can keep the previous images if they were not changed, if they were  not touched.  Docker doesn't need to rebuild them anymore.  And now we have this drawer, and this drawer, as soon as you change a library, as soon as  you change a class file, as soon as you change anything, you need to rebuild the whole Docker  image.  So, how can we see that?  Well there is a nice stuff called dive.  So what we will do is we will dive into the image, so I will stop it here, and I will  dive into sprint in Docker 1.0.  Takes a bit of time, of course.  And here, this is how it looks.  So here I inherit from this base image, you can see there was some installation going  on, and here I was not the one to copy several things, and here this is what I did, this  is what I added to my image, this is this mvn package where I skipped the tests.  All the rest is in the returns from the previous image.  And here, this is the issue that this mvnw package adds the whole stuff on the other  side, and as soon as I change anything, well it will rebuild the whole image, including  fetching the dependencies.  As I mentioned, you need to redo everything, and parts of the build is to fetch the dependencies.  And it's not great fetching the dependencies, as you know that Maven, but every build tool  that relies on all the jobs, they don't load the whole internet, and through alternative  dependencies here, you can see that there are a lot here.  So every transitive dependency is here, and we don't want that.  So how can we do better?  One way to do better is actually to do multi-stage builds.  And multi-stage builds allows you to, inside the Sandocker file, to change the base image.  So you can copy a file from a previous stage into your current stage.  This is pretty widespread, it's really a great thing to use, but if you are using Skaffold,  Skaffold is the tool that lets you configure your Docker to automatically publish the Kubernetes,  either locally or micremote, probably locally.  So the problem of multi-stage builds is they are not compatible with Skaffold, so be careful  about that.  Otherwise, this is how it looks like.  I will need to quit that.  Please let me quit.  Yes.  And I will just use the multi-layer here.  So now I have two different layers.  I have two different stages.  Sorry about the semantics.  The first stage is about building and I'm using a JDK.  And once I get the drawer, I use another stage where I use single JRE and I copy the drawer  from the previous one into this image.  So this ID here and it's 1.1 and if we dive into it, as you can see, here I have like  many, many less layers and more importantly, here I have one single drawer.  I don't have the dependencies issue that I had before, which is a bit better.  Still I have this drawer layer and the problem of this drawer layer, again, if I change a  class file, which will happen all the time, I will need to rebuild everything.  I don't need to fetch the dependencies, but I need to rebuild the drawer.  So the next ID is to start thinking about the drawer as a distribution unit, but actually  your Docker image is also a distribution unit and they are pretty redundant.  Without Docker, JAR is the best distribution unit you can get, but with Docker images,  they are useless because running a JAR inside Docker image, why don't we just run the JAR  exploded?  And that's the train of thoughts of the people behind the JIP plugin, which is a standard  of Mavin plugin.  And with JIP, you just like configure your Mavin PON to use JIP and it also works with  Gradle and it can either push to a remote Docker repository or to the local Docker.  You can add a lot of configuration options, including choosing the parent image and it  runs the exploded JAR and there are a lot of benefits.  Well the first one is there is no Docker file because probably all your Java Docker files  will look the same, so it's no use repeating them over and over.  The second benefit is you have an automated version and from the POM.  So if your POM is version X, then JIP knows that it will create version X of the Docker  image.  And then there are different layers.  So there are dependencies and resources and compile code, meaning that the compile code  is the stuff that changes the most.  And there is no issue, well there is not, it's not useful to every time get the dependency  again or copy the resources again if only your code changes.  So this layered way of doing things is actually the best because if you change the compile  code, the code, you compile the code, you only change the utmost layer.  Depending on how you look at things, the one which is at the bottom or at the top, whether  your parent is at the top or at the bottom.  So that's pretty smart and actually in the latest version, you have four layers, one  is about snapshot dependencies because snapshot dependencies are supposed to change more often  than simple dependencies.  And so it handles that for you.  So let's do that and let's see that here in this, here I've removed the Docker file entirely,  there is no Docker file anymore, but I've added the JIP-Maven plugin.  And here I'm telling it that it will create the image and as you can see it syncs the  image version with the project version and the layers look actually very nice.  So I need to remember its version 2.0, I will dive into it.  And here you can see that here you've got the parent stuff and here you've got the JIP-Maven  plugin, the first one is about the libraries, the second one is about the snapshot libraries,  again because they change more often.  The third one is about the resources, so I have the files and the final one is about  the compiler code.  So if I need, just need to change the compiler code, all the previous layers will be kept  and the building of the image will be much, much faster.  Also with JIP you can configure and for example to change the parent image.  So here I will be using an Alpine gearing to keep the size of my final image smaller  and if I do a Docker image is grab, spring in Docker.  We can see here that I have Russian 2.0 and 2.1 and the 2.1 uses the Alpine parent image.  So you can see I have a few megabytes which can be a good thing if you've got a small  image you want to put them very small.  The next option if you've got a spring boot application is you skip the JIP plugin entirely  and use the spring boot mechanism.  And spring boot is also able to create a Docker image that is nicely layered by default to  have like the same ones as in JIP, the dependencies, the resource, the snapshot dependencies and  the compile code, different order though, but you can customize them.  So if you know that some parts of your applications are going to change much more often than others,  you can have a dedicated file, an index file that tells you, hey, this package or this  stuff you will put it into this layer and this other into this layer.  In order to do that, however, we need to get back to a Docker file.  So here is how you do it with the spring.  And I have a Docker file again and it's like a multi-stage build.  You can see here you have three stages.  The first one creates the package, creates the JAR.  Then you have a second one that actually explodes the JAR and the third one will actually run  the application through like a specific spring boot mechanism.  This is a lot of work, but instead of doing that by ourselves, it would be much, much  better to let somebody else do it.  And that's the ID behind the BuildPacks.  BuildPacks are backed by the Cloud Native Foundation and it's a tool that's able to understand  how to build your project.  So if you remember, if you have been doing cloud stuff since some years, you might know  about Iroku.  And when you were using Iroku, you would just like push, you would get push your sources  to Iroku.  And then Iroku on their own servers would try to sniff what kind of project it was and  build it accordingly.  So it would say, oh, I see a pump file.  So it's probably a Maven project.  So I will build it with Maven.  And right now, this like way to do stuff was finally seemed to be worthwhile.  And Iroku was joined by a team where PyVol told Tensu, I don't remember what their name  is right now.  They were joined by Spring.  Now they both provide this like standard of BuildPacks.  You've got a lot of configuration options.  But you can pretty much do what you want with some limitation, of course.  So the ID is that you've got no Docker file.  It's scaffold friendly, and because I'm using the Springboots project, it knows it's a Springboot  project.  So it can use the previous four layers or if you customized it, all the layers.  However, there are some downsides as well.  Again, we get back to the fact that there is no sync between the pump and the Docker  image.  It's not that there is no choice of the parent image.  It's a very limited choice.  And if you want to change the parent image, you need to put an image that is compatible  and I try to dive into it.  It's just not some labels.  It seems to be a pretty, pretty complex and dedicated process.  I've noticed it takes a long time, even without changes.  So perhaps I mismanage it, but for me, it's very long.  It's especially very long because the build images, they don't move it every time.  So every time it downloads the TDK again.  So you can do it by running, first you need to install the build pack, then you run pack,  you set the parent builder, sorry, and it will infer the build image.  But why do that?  With the latest version of Springboots, we can do the same inside of Maven or Gradle  and it will use it under the cover.  So how do we do it?  It's quite easy.  We just run, sorry, not here, here.  We just run spring, mdm, always mdm.  Springboots builds image.  Again, the process is very, very long, so I won't show it to you.  And in the end, this is what we get.  We got rid of the Docker file again.  And here we have the layer configuration enabled.  And as you can see, nothing had changed otherwise.  So the plugin itself knows how to create Docker images.  That's pretty good.  And the really, really nice stuff is that there are configuration options and it's very  easy to change the configuration option to use Graal VM and to build a Graal VM native  image.  For example, here, that's the last stuff I want to show you, to create a native image  that just adds an environment variable, the dp-boots-native-image-true, and that's done.  And if we check again about the size of the images, so they are here.  Here you have the Springboots inside a GVM.  And here you have the Springboots inside a normal native process.  So if you are interested in cloud stuff, if you want to have fast startup time and low  memory consumption, then you can just use these kind of images.  And it's a wrap.  I just want to finish by mentioning the Docker squash.  You can try to flatten the layers.  In this simple application, I tried to flatten the layers from 1.0 and it was less than one  person gain of the size.  And from the 5.0, it was again less than one person gain of the size.  So depending on what you value, if you value faster build time or smaller image sizes, you  might want to check Docker squash.  Might depend on your use case and probably on your parent images.  But with the sample, my layers were nicely done so Docker squash doesn't bring any benefit.  So as a recap, when you want to Dockerize your Java application, you must think about  several things.  You must think about the sync of the version between the pump and the image.  You must think about organizing the layers to develop faster for your builds to be faster.  And probably forget about squash though it might have seemed super popular at some point.  It's not anymore.  So thanks for listening to me.  You can read my blog, you can follow me on Twitter and more importantly, if you want  to try everything that I do here, everything is on Github.  So thanks a lot and have a good day.
hitchhikercontainer_0_179.png hitchhikercontainer_180_491.png hitchhikercontainer_492_875.png hitchhikercontainer_876_4967.png hitchhikercontainer_4968_5579.png hitchhikercontainer_5580_5819.png hitchhikercontainer_5820_5879.png hitchhikercontainer_5880_11531.png hitchhikercontainer_11532_15275.png hitchhikercontainer_15276_17579.png hitchhikercontainer_17580_17735.png hitchhikercontainer_17736_17819.png hitchhikercontainer_17820_20879.png hitchhikercontainer_20880_21599.png hitchhikercontainer_21600_23843.png hitchhikercontainer_23844_26579.png hitchhikercontainer_26580_27083.png hitchhikercontainer_27084_27539.png hitchhikercontainer_27540_27971.png hitchhikercontainer_27972_32219.png hitchhikercontainer_32220_32327.png hitchhikercontainer_32328_33815.png hitchhikercontainer_33816_33863.png hitchhikercontainer_33864_40079.png hitchhikercontainer_40080_41207.png hitchhikercontainer_41208_42455.png hitchhikercontainer_42456_44891.png hitchhikercontainer_44892_45785.png
Hi folks, welcome to this talk about containerizing Java applications. I'm Nicola Frantl, I've been a former developer, architect, whatever, and right now I work as a developer advocate. Since the trend about containerization started, I became very interested in Docker and Kubernetes and all the surrounding technologies. I work for a company called Hazelcast, and Hazelcast has two products. The main one is an in-memory data grid, and you can think about an in-memory data grid as distributed data data structures. So you can short your data and replicate your data over different nodes over the network. The second one is Hazelcast chat, and it's an in-memory stream processing engine, so it leverages the distributed nature of Hazelcast's time-dg to do this stuff. Today I won't talk about Hazelcasts, I will just use it, and I will talk about how we can containerize our Java app. So let's start with a non-Java app, with a Python application. And how do you containerize a Python application? Well, it's quite straightforward, actually. You iterate from Python image, you add your file that contains the dependencies, you install them, you add the script itself, and then you run the scripts, and it's done. LikeWIFE for a Node.js application, you iterate from your nodes, and there's image, you copy the file that contains the dependencies, you install the dependencies, and you run again the script. And all of those scripting languages have in common is that, well, there is no intermediate bytecode, there is no compile phase. So the main gist is to iterate from the right image, to iterate the dependencies, and to run the script. It's wussy-weir, what you see is what you run. Whereas in Java, it's slightly more complicated, because we have several layers. Well, the first one is actually the JAR that might contain all the JAR, depending on how you want to run it. And here I'm using HazelCost as a dependency, but again, it's not warranted. Then you have your jivm, and then you have your Docker. And in my sample app, I have a REST endpoint, I want to expose the REST endpoint to the outside world, so I can put and get data in HazelCost. So how do we do it? Well the easy way is we create the JAR outside of Docker, so we just mvn a clean package, and then we have a Docker file that wraps it. Let's do it. So here I have my application. It's a string boot application, because in the end, I will show you how you can use it with string boot, but it has only to dependency the web for the REST endpoint and HazelCosts. And then nothing mind-blowing. It's just very straightforward, and the Docker file is just, hey, just copy the JAR file and put it in Docker, and then run java.dashjar the Docker JAR. So here I will be caging the application and the n clean package. Then I will build the Docker from with the Docker file, and because I'm lazy, I will just do that. I won't do it every time, because in the end, in some alternatives, it can take quite some time. Here it's pretty straightforward, but later options can be quite time-consuming. And now I can Docker run dash dash or M, because I don't want to keep it in the end, and P, because I want to be to expose the ports to the outside world, and it's called string in Docker column five. So the application starts. I have HazelCosts running in the background, and now I can check and I will put some data first, exposed HTTP, localhosts, 8080, let me say world, it's put, because I don't know my own application. Now it works, and then I can curl, edge, to check that the data is still there, 8080, and yes, it's here, I can put another stuff and say hello, Nikola. And yes, so I have my data. So that's the straightforward things to do, and it works. But the issue here is that we actually need to build first jar outside of Docker, and then we use Docker to package the jar, which is not super straightforward. So the next step is to run the entire build inside of Docker. So here I have my updated Docker file, and to run the build inside of Docker, we actually needs to have a compiler. So we inherit from a GDK and not a simple GRE, and we do our like, black magic, we copy the MVN folder, because we are supposed to be independent, and we run the package, skip the test, because of course there are no tests, but anyway, and it works pretty well. And now if we do the same, so I will get back to the terminal, I will stop this one, I will run 1.0, because I already like created it before. And now I can check that it still works. So it's the data that is still there, everything works as expected. And yes, it works, but there are several downsides, and the biggest downsides is we embed a GDK in the final image, and it has two, like, big issues. The first is an increased size, and we want our Docker image to be as small as possible, when possible, and the biggest issue is actually the fact that it can compile Java code, so you will actually deliver in production an image that can compile code, and it's a huge security issue, you never want to do that. The second problem is about version handling. As you noticed, I need myself to set the version when I build the Docker image, so I need to remember that this version of the POM is the same as the version of the Docker build, I need to keep them both in sync at the same time. And finally, there is no layering, so what do I mean by that? You know that Docker images are layered, so you inherit from the base image, and you add some more stuff, which creates another image, this image itself can be a parent image, and so on and so forth. And the idea is that you can keep the previous images if they were not changed, if they were not touched. Docker doesn't need to rebuild them anymore. And now we have this drawer, and this drawer, as soon as you change a library, as soon as you change a class file, as soon as you change anything, you need to rebuild the whole Docker image. So, how can we see that? Well there is a nice stuff called dive. So what we will do is we will dive into the image, so I will stop it here, and I will dive into sprint in Docker 1.0. Takes a bit of time, of course. And here, this is how it looks. So here I inherit from this base image, you can see there was some installation going on, and here I was not the one to copy several things, and here this is what I did, this is what I added to my image, this is this mvn package where I skipped the tests. All the rest is in the returns from the previous image. And here, this is the issue that this mvnw package adds the whole stuff on the other side, and as soon as I change anything, well it will rebuild the whole image, including fetching the dependencies. As I mentioned, you need to redo everything, and parts of the build is to fetch the dependencies. And it's not great fetching the dependencies, as you know that Maven, but every build tool that relies on all the jobs, they don't load the whole internet, and through alternative dependencies here, you can see that there are a lot here. So every transitive dependency is here, and we don't want that. So how can we do better? One way to do better is actually to do multi-stage builds. And multi-stage builds allows you to, inside the Sandocker file, to change the base image. So you can copy a file from a previous stage into your current stage. This is pretty widespread, it's really a great thing to use, but if you are using Skaffold, Skaffold is the tool that lets you configure your Docker to automatically publish the Kubernetes, either locally or micremote, probably locally. So the problem of multi-stage builds is they are not compatible with Skaffold, so be careful about that. Otherwise, this is how it looks like. I will need to quit that. Please let me quit. Yes. And I will just use the multi-layer here. So now I have two different layers. I have two different stages. Sorry about the semantics. The first stage is about building and I'm using a JDK. And once I get the drawer, I use another stage where I use single JRE and I copy the drawer from the previous one into this image. So this ID here and it's 1.1 and if we dive into it, as you can see, here I have like many, many less layers and more importantly, here I have one single drawer. I don't have the dependencies issue that I had before, which is a bit better. Still I have this drawer layer and the problem of this drawer layer, again, if I change a class file, which will happen all the time, I will need to rebuild everything. I don't need to fetch the dependencies, but I need to rebuild the drawer. So the next ID is to start thinking about the drawer as a distribution unit, but actually your Docker image is also a distribution unit and they are pretty redundant. Without Docker, JAR is the best distribution unit you can get, but with Docker images, they are useless because running a JAR inside Docker image, why don't we just run the JAR exploded? And that's the train of thoughts of the people behind the JIP plugin, which is a standard of Mavin plugin. And with JIP, you just like configure your Mavin PON to use JIP and it also works with Gradle and it can either push to a remote Docker repository or to the local Docker. You can add a lot of configuration options, including choosing the parent image and it runs the exploded JAR and there are a lot of benefits. Well the first one is there is no Docker file because probably all your Java Docker files will look the same, so it's no use repeating them over and over. The second benefit is you have an automated version and from the POM. So if your POM is version X, then JIP knows that it will create version X of the Docker image. And then there are different layers. So there are dependencies and resources and compile code, meaning that the compile code is the stuff that changes the most. And there is no issue, well there is not, it's not useful to every time get the dependency again or copy the resources again if only your code changes. So this layered way of doing things is actually the best because if you change the compile code, the code, you compile the code, you only change the utmost layer. Depending on how you look at things, the one which is at the bottom or at the top, whether your parent is at the top or at the bottom. So that's pretty smart and actually in the latest version, you have four layers, one is about snapshot dependencies because snapshot dependencies are supposed to change more often than simple dependencies. And so it handles that for you. So let's do that and let's see that here in this, here I've removed the Docker file entirely, there is no Docker file anymore, but I've added the JIP-Maven plugin. And here I'm telling it that it will create the image and as you can see it syncs the image version with the project version and the layers look actually very nice. So I need to remember its version 2.0, I will dive into it. And here you can see that here you've got the parent stuff and here you've got the JIP-Maven plugin, the first one is about the libraries, the second one is about the snapshot libraries, again because they change more often. The third one is about the resources, so I have the files and the final one is about the compiler code. So if I need, just need to change the compiler code, all the previous layers will be kept and the building of the image will be much, much faster. Also with JIP you can configure and for example to change the parent image. So here I will be using an Alpine gearing to keep the size of my final image smaller and if I do a Docker image is grab, spring in Docker. We can see here that I have Russian 2.0 and 2.1 and the 2.1 uses the Alpine parent image. So you can see I have a few megabytes which can be a good thing if you've got a small image you want to put them very small. The next option if you've got a spring boot application is you skip the JIP plugin entirely and use the spring boot mechanism. And spring boot is also able to create a Docker image that is nicely layered by default to have like the same ones as in JIP, the dependencies, the resource, the snapshot dependencies and the compile code, different order though, but you can customize them. So if you know that some parts of your applications are going to change much more often than others, you can have a dedicated file, an index file that tells you, hey, this package or this stuff you will put it into this layer and this other into this layer. In order to do that, however, we need to get back to a Docker file. So here is how you do it with the spring. And I have a Docker file again and it's like a multi-stage build. You can see here you have three stages. The first one creates the package, creates the JAR. Then you have a second one that actually explodes the JAR and the third one will actually run the application through like a specific spring boot mechanism. This is a lot of work, but instead of doing that by ourselves, it would be much, much better to let somebody else do it. And that's the ID behind the BuildPacks. BuildPacks are backed by the Cloud Native Foundation and it's a tool that's able to understand how to build your project. So if you remember, if you have been doing cloud stuff since some years, you might know about Iroku. And when you were using Iroku, you would just like push, you would get push your sources to Iroku. And then Iroku on their own servers would try to sniff what kind of project it was and build it accordingly. So it would say, oh, I see a pump file. So it's probably a Maven project. So I will build it with Maven. And right now, this like way to do stuff was finally seemed to be worthwhile. And Iroku was joined by a team where PyVol told Tensu, I don't remember what their name is right now. They were joined by Spring. Now they both provide this like standard of BuildPacks. You've got a lot of configuration options. But you can pretty much do what you want with some limitation, of course. So the ID is that you've got no Docker file. It's scaffold friendly, and because I'm using the Springboots project, it knows it's a Springboot project. So it can use the previous four layers or if you customized it, all the layers. However, there are some downsides as well. Again, we get back to the fact that there is no sync between the pump and the Docker image. It's not that there is no choice of the parent image. It's a very limited choice. And if you want to change the parent image, you need to put an image that is compatible and I try to dive into it. It's just not some labels. It seems to be a pretty, pretty complex and dedicated process. I've noticed it takes a long time, even without changes. So perhaps I mismanage it, but for me, it's very long. It's especially very long because the build images, they don't move it every time. So every time it downloads the TDK again. So you can do it by running, first you need to install the build pack, then you run pack, you set the parent builder, sorry, and it will infer the build image. But why do that? With the latest version of Springboots, we can do the same inside of Maven or Gradle and it will use it under the cover. So how do we do it? It's quite easy. We just run, sorry, not here, here. We just run spring, mdm, always mdm. Springboots builds image. Again, the process is very, very long, so I won't show it to you. And in the end, this is what we get. We got rid of the Docker file again. And here we have the layer configuration enabled. And as you can see, nothing had changed otherwise. So the plugin itself knows how to create Docker images. That's pretty good. And the really, really nice stuff is that there are configuration options and it's very easy to change the configuration option to use Graal VM and to build a Graal VM native image. For example, here, that's the last stuff I want to show you, to create a native image that just adds an environment variable, the dp-boots-native-image-true, and that's done. And if we check again about the size of the images, so they are here. Here you have the Springboots inside a GVM. And here you have the Springboots inside a normal native process. So if you are interested in cloud stuff, if you want to have fast startup time and low memory consumption, then you can just use these kind of images. And it's a wrap. I just want to finish by mentioning the Docker squash. You can try to flatten the layers. In this simple application, I tried to flatten the layers from 1.0 and it was less than one person gain of the size. And from the 5.0, it was again less than one person gain of the size. So depending on what you value, if you value faster build time or smaller image sizes, you might want to check Docker squash. Might depend on your use case and probably on your parent images. But with the sample, my layers were nicely done so Docker squash doesn't bring any benefit. So as a recap, when you want to Dockerize your Java application, you must think about several things. You must think about the sync of the version between the pump and the image. You must think about organizing the layers to develop faster for your builds to be faster. And probably forget about squash though it might have seemed super popular at some point. It's not anymore. So thanks for listening to me. You can read my blog, you can follow me on Twitter and more importantly, if you want to try everything that I do here, everything is on Github. So thanks a lot and have a good day.
talk about how we can containerize our Java app. change anything, you need to rebuild the whole builds is they are not compatible with Skaffold, many, many less layers and more importantly, here time, I will need to rebuild everything. I don't you can get, but with Docker images, they are there are different layers. So there are the code, you only change the utmost layer. Depending on how you look at things, the one kept and the building of the image will be much, image is grab, spring in Docker. We can see here spring boot is also able to create a Docker image however, we need to get back to a Docker file. So JAR. Then you have a second one that actually are backed by the Cloud Native Foundation and their own servers would try to sniff what kind of layers. In this simple application, I tried to Dockerize your Java application, you must think thanks a lot and have a good day.