{"id":202,"date":"2020-09-01T14:19:49","date_gmt":"2020-09-01T13:19:49","guid":{"rendered":"http:\/\/jowisoftware.de\/wp\/?p=202"},"modified":"2021-04-18T21:28:52","modified_gmt":"2021-04-18T20:28:52","slug":"building-small-jres-and-docker-images-with-recent-versions-of-openjdk-part-2","status":"publish","type":"post","link":"https:\/\/jowisoftware.de\/wp\/2020\/09\/building-small-jres-and-docker-images-with-recent-versions-of-openjdk-part-2\/","title":{"rendered":"Building Small JREs and Docker Images with Recent Versions of OpenJDK &#8211; Part 2"},"content":{"rendered":"\n<p>This is part two of my article about small JREs. You find the first part in<a href=\"https:\/\/jowisoftware.de\/wp\/2020\/09\/building-small-jres-and-docker-images-with-recent-versions-of-openjdk-part-1\/\" data-type=\"post\" data-id=\"172\"> a separate blog post<\/a>. The original post was published in German <a href=\"https:\/\/www.cologne-intelligence.de\/blog\/detail\/show\/kleine-jres-mit-openjdk-13-erstellen\/\" data-type=\"URL\" data-id=\"https:\/\/www.cologne-intelligence.de\/blog\/detail\/show\/kleine-jres-mit-openjdk-13-erstellen\/\">in my employers blog<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">What about Docker?<\/h2>\n\n\n\n<p>In the first part, we build a small JRE. Now we want to derive a Docker image from that JRE. Let&#8217;s start with a naive approach and write a simple Dockerfile:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FROM ubuntu:latest\n\nCOPY target\/jlink1b \/app\nCMD \/app\/bin\/app<\/pre>\n\n\n\n<p>The image is very easy to build and to start. With 101 Megabytes it is relatively small.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">$ docker build -t test .\nSending build context to Docker daemon  131.9MB\nStep 1\/3 : FROM ubuntu:latest\n ---&amp;gt; cf0f3ca922e0\nStep 2\/3 : COPY target\/jlink1 \/app\n ---&amp;gt; 1105c7ca04cb\nStep 3\/3 : CMD \/app\/bin\/app\n ---&amp;gt; Running in 517a84b22477\nRemoving intermediate container 517a84b22477\n ---&amp;gt; 1bd40086a3b9\nSuccessfully built 1bd40086a3b9\nSuccessfully tagged test:latest\n$ docker run --rm -it test\nRunning in module path? yes\nThe website 'https:\/\/www.jowisoftware.de' has 85 lines of HTML code<\/pre>\n\n\n\n<p>It is a bit unpleasant that the image has 60 Megabytes overhead compared to the JRE itself. The reason for this is the large Ubuntu base image. I claim that (at least in the rigid interpretation of containers) a normal docker container does not need <code>apt-get<\/code>, <code>bash<\/code> and <code>wget<\/code> during the normal operations. In this article, I&#8217;d like to carefully give a proposal on how to build even smaller and more secure containers.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A minimal Docker Image<\/h2>\n\n\n\n<p>The idea is simple: the JRE already contains everything we need to start the software. But the JRE itself also has dependencies against <code>libc<\/code> and <code>zlib<\/code>. But these are the only requirements &#8211; nothing else is needed! Can&#8217;t we build an image with only these files? By the way, <a href=\"https:\/\/github.com\/GoogleContainerTools\/distroless\">Google&#8217;s Distroless-Project<\/a> also follows exactly this idea.<\/p>\n\n\n\n<p>The previously used Ubuntu image already contains the <code>libc<\/code> and <code>zlib<\/code>. Thankfully, the system is able to tell us which files belong to these libraries &#8211; the installer (<code>dpkg<\/code>) holds this information in text file to be able to uninstall the package. What will happen, if we clone these files in an empty Docker image?<\/p>\n\n\n\n<p>Let&#8217;s try with another Dockerfile:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FROM ubuntu:latest AS builder\n\n# copy files from libc\/zlib, skip directories and docs\nRUN mkdir \/target; \\\n  for lib in \/var\/lib\/dpkg\/info\/{libc6,zlib1g}:amd64.list; \\\n  do \\\n    while IFS='' read -r file; do \\\n      if [ -f \"$file\" ]; then \\\n        dir=\"\/target\/$(dirname $file)\"; \\\n        mkdir -p \"$dir\"; \\\n        cp -d --preserve=all \"$file\" \"$dir\"; \\\n      fi; \\\n    done &amp;lt; \"$lib\"; \\\n  done; \\\n  rm -rf \/target\/usr\/share\/doc \/target\/usr\/share\/lintian\n\n\n\n# start with an empty image\nFROM scratch\n\n# copy libc and zlib\nCOPY --from=builder \/target \/\n# copy our app\nCOPY target\/jlink1b \/app\n\nUSER 65534\nCMD \"\/app\/bin\/app\"<\/pre>\n\n\n\n<p>The image builds, but starting a container fails:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">$ docker run --rm -it test\ndocker: Error response from daemon: OCI runtime create failed:\ncontainer_linux.go:346: starting container process caused\n\"exec: \\\"\/bin\/sh\\\": stat \/bin\/sh: no such file or directory\": unknown.<\/pre>\n\n\n\n<p>We&#8217;re hitting <em>two<\/em> problems here: First, Docker tries to execute the <code>CMD<\/code>-directive with <code>\/bin\/sh<\/code>. We did not include <code>sh<\/code> in our image! We can work around this problem if we provide an argument array instead of a string. Docker will then take the first argument as binary and uses the rest of the array as arguments.<\/p>\n\n\n\n<p>The second problem is not immediately visible: the file <code>target\/bin\/app<\/code> itself is also a shell script! If we take a look at it&#8217;s content, we notice that don&#8217;t really need the script at all and we can invoke <code>java<\/code> directly. We change the second half of our Dockerfile:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FROM scratch\n\nCOPY --from=builder \/target \/\nCOPY target\/jlink1b \/app\n\nUSER 65534\nENTRYPOINT [ \"\/app\/bin\/java\" ]\nCMD [ \"-m\", \"jowisoftware.jre\/de.jowisoftware.learning.jre.Main\" ]<\/pre>\n\n\n\n<p>This approach works! The image is 48 Megabytes large.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">$ docker run --rm -it test2\nRunning in module path? yes\nThe website 'https:\/\/www.jowisoftware.de' has 85 lines of HTML code<\/pre>\n\n\n\n<p>We could also use the normal classpath here. In this case, we have to adapt <code>ENTRYPOINT<\/code> and <code>CMD<\/code>. We&#8217;ll do a full example in the next section of this post.<\/p>\n\n\n\n<p>If an intruder gains access to the container, he can&#8217;t simply install tcpdump or execute arbitrary shell scripts. In addition, we only have to recreate the image if <code>glibc<\/code>, <code>zlib<\/code> or java gets updated because there are no other components that could receive updates.<\/p>\n\n\n\n<p>On the other side, debugging gets harder. Patterns like sidecar containers can help here. Another approach would be to <em>temporarily<\/em> include a shell and additional software. Tools like Spring Boot Actuator or JMX work without any additional software in the container. As always, you have to decide what is most valuable for your case.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">A Full Example<\/h2>\n\n\n\n<p>Let&#8217;s summarize what we learned so far with a complete example:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">FROM ubuntu:latest AS jre\n\nRUN apt-get -y update &amp;amp;&amp;amp; \\\n  apt-get -y upgrade &amp;amp;&amp;amp; \\\n  apt-get -y install curl binutils\n\nENV RELEASE jdk-14.0.2+12\nENV CHECKSUM 7d5ee7e06909b8a99c0d029f512f67b092597aa5b0e78c109bd59405bbfa74fe\n\n# download openjdk\nRUN mkdir -p \/tmp\/jdk\/ &amp;amp;&amp;amp; \\\n  cd \/tmp\/jdk &amp;amp;&amp;amp; \\\n  url=\"https:\/\/api.adoptopenjdk.net\/v3\/binary\/version\/$RELEASE\/linux\/x64\/jdk\/hotspot\/normal\/adoptopenjdk?project=jdk\" &amp;amp;&amp;amp; \\\n  curl -s -L -o jdk.tgz \"$url\" &amp;amp;&amp;amp; \\\n  echo \"$CHECKSUM jdk.tgz\" &amp;gt; SHA256SUMS &amp;amp;&amp;amp; \\\n  sha256sum --status --strict -c SHA256SUMS\n\n# copy libc and zlib\nRUN for lib in libc6 zlib1g; do \\\n    while IFS='' read -r file; do \\\n      if [ -f \"$file\" ]; then \\\n        dir=\"\/target\/$(dirname $file)\"; \\\n        mkdir -p \"$dir\"; \\\n        cp -d --preserve=all \"$file\" \"$dir\"; \\\n      fi; \\\n    done &amp;lt; \"\/var\/lib\/dpkg\/info\/$lib:amd64.list\"; \\\n  done; \\\n  rm -rf \/target\/usr\/share\/doc \/target\/usr\/share\/lintian; \\\n  mkdir -p \/target\/tmp \/target\/etc; \\\n  chmod 777 \/target\/tmp &amp;amp;&amp;amp; chmod +t \/target\/tmp\n\n# create users root and nobody\nRUN echo \"root:x:0:\" &amp;gt; \/target\/etc\/group; \\\n    echo \"nogroup:x:65534:\" &amp;gt;&amp;gt; \/target\/etc\/group; \\\n    echo \"root:x:0:0:root:\/root:\/bin\/bash\" &amp;gt; \/target\/etc\/passwd; \\\n    echo \"nobody:x:65534:65534:nobody:\/tmp:\/usr\/sbin\/nologin\" &amp;gt;&amp;gt; \/target\/etc\/passwd\n\n# create jre (with all java.se modules)\nRUN tar -C \/tmp\/jdk --strip-components=1 -xzf \/tmp\/jdk\/jdk.tgz &amp;amp;&amp;amp; \\\n\t\/tmp\/jdk\/bin\/jlink \\\n\t--add-modules java.se \\\n\t--output \/target\/jre \\\n\t--no-header-files \\\n\t--no-man-pages \\\n\t--strip-debug \\\n\t--compress=2\n\n\n\nFROM maven:3-openjdk-14-slim AS build\n\nWORKDIR \/app\n\n# cache dependencies as long as pom.xml does not change\nCOPY pom.xml .\/\nRUN echo \"Pre-caching mvn artifacts...\" &amp;amp;&amp;amp; \\\n mvn de.qaware.maven:go-offline-maven-plugin:1.2.5:resolve-dependencies || true\n\n# build project offline\nCOPY . .\nRUN mvn -o package\nRUN mkdir -p \/target &amp;amp;&amp;amp; \\\n    mv target\/app.jar \/target\n\n\n\nFROM scratch\n\n# copy jre &amp;amp; app\nCOPY --from=jre \/target \/\nCOPY --from=build \/target \/app\n\n# run java as \"nobody\"\nUSER 65534\nENTRYPOINT [ \"\/jre\/bin\/java\" ]\nCMD [ \"-jar\", \"\/app\/app.jar\" ]<\/pre>\n\n\n\n<p>We start with a Docker container that has the sole purpose to building our JRE. First (lines 3-5) we install some dependencies, then (lines 7-16) we Download our required Version of the OpenJDK. We use AdoptOpenJDK here.<\/p>\n\n\n\n<p>The lines 19-28 copy our dependencies into <code>\/target<\/code>.<\/p>\n\n\n\n<p>We also create a <code>\/tmp<\/code> directory because our software might expect one to exist\u2026 In addition, we create a user \u201eroot\u201c and a user \u201enobody\u201c (note that the login shells are invalid because none of the binaries exist). These steps might not be necessary and depend on your use case.<\/p>\n\n\n\n<p>In the last step we create our tailored JRE (lines 39-46).<\/p>\n\n\n\n<p>The next Container uses Maven to build the application. We cache the Maven artifacts (lines 55-57). At least as long as the <code>pom.xml<\/code> does not change, this saves us much time and bandwidth. This step is also fully optional. Then we copy the whole project into the container and build the app using Maven (lines 60-63).<\/p>\n\n\n\n<p>Now we finally create our minimal image: we copy the JRE with its dependencies and the built artifacts into an empty container, switch to a non-root user as default and set the correct entrypoint.<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"dockerfile\" data-enlighter-theme=\"\" data-enlighter-highlight=\"\" data-enlighter-linenumbers=\"\" data-enlighter-lineoffset=\"\" data-enlighter-title=\"\" data-enlighter-group=\"\">$ docker run --rm -it test\nRunning in module path? no\nThe website 'https:\/\/www.jowisoftware.de' has 85 lines of HTML code<\/pre>\n\n\n\n<p>The example is 68 Megabytes in size! It runs as non-root and contains only our JRE. It uses the classic classpath and all modules from <code>java.se<\/code>.<\/p>\n\n\n\n<p>You can use this Dockerfile as a starting point for your own experiments.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">My Own Docker Image<\/h2>\n\n\n\n<p>Instead of creating the JRE on every build it is also possible to use such a minimal Docker image as parent image in the last <code>FROM<\/code> directive (line 67).<\/p>\n\n\n\n<p>I&#8217;m doing this for my personal projects. I built a parent Docker image using the scripts in <a href=\"https:\/\/github.com\/jochenwierum\/openjdk-minimal-jre\">my github repository<\/a>. The Images are published on <a href=\"https:\/\/hub.docker.com\/r\/jochenwierum\/openjdk-minimal-jre\" data-type=\"URL\" data-id=\"https:\/\/hub.docker.com\/r\/jochenwierum\/openjdk-minimal-jre\">Docker hub<\/a>. In my JRE I included <code>java.se<\/code> and <code>jdk.unsupported<\/code>. This is fine for my personal projects. YMMV. Again: You&#8217;re welcome to use this project as a template to build your own JRE.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p>I hope that I was able to show that it&#8217;s not <em>that<\/em> hard to build tailored JREs from recent JDKs. With full java modules it is really simple. Using the old classpath, we have to walk the extra mile and collect the required modules.<\/p>\n\n\n\n<p>I&#8217;d like to invite you to reflect, if such a way might be useful for your projects. Creating minimal docker images has many advantages, but debugging is more difficult. Projects like Spring Boot Actuator can help (use a separate port and do not expose it into the public world!). Nevertheless, we have to re-think some of our old concepts. You still have to think about if this is a sensible way for your development environment.<\/p>\n\n\n\n<p>I had fun with these experiments and I hope that I was able to share some of my insights \ud83d\ude42<\/p>\n","protected":false},"excerpt":{"rendered":"<p>This is part two of my article about small JREs. You find the first part in a separate blog post. The original post was published in German in my employers blog. What about Docker? In the first part, we build &#8230; <a class=\"more-link\" href=\"https:\/\/jowisoftware.de\/wp\/2020\/09\/building-small-jres-and-docker-images-with-recent-versions-of-openjdk-part-2\/\">Read More &raquo;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[40,4,20],"tags":[],"class_list":["post-202","post","type-post","status-publish","format-standard","hentry","category-docker","category-7-english","category-2-java"],"_links":{"self":[{"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/posts\/202","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/comments?post=202"}],"version-history":[{"count":14,"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/posts\/202\/revisions"}],"predecessor-version":[{"id":233,"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/posts\/202\/revisions\/233"}],"wp:attachment":[{"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/media?parent=202"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/categories?post=202"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/jowisoftware.de\/wp\/wp-json\/wp\/v2\/tags?post=202"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}