This lab will walk you through steps to build container images with various technologies.
-
Slides: https://speakerdeck.com/maeddes/options-galore-from-source-code-to-container-image
-
Recording (English): https://www.youtube.com/watch?v=mFamovTQ6Po
-
Recording (German): https://www.youtube.com/watch?v=ga8iqQ25lUY
Mandatory:
-
A Docker environment and Docker CLI https://docs.docker.com/get-docker/
-
Pack CLI for Cloud-Native Buildpacks https://buildpacks.io/docs/tools/pack/
-
Clone/Download this repo: https://github.com/maeddes/options-galore-container-build
-
Access List: https://docs.google.com/spreadsheets/d/1wG4CD1FUPxoZRNqN41Mb_UucvALF3ast5lnUVCKCQPc/edit?usp=sharing
Recommended:
-
A Java (11 or later) Development Kit for Java examples, e.g https://adoptopenjdk.net/
Optional:
-
Dive tool https://github.com/wagoodman/dive
Validate docker installation.
docker version
Should display output like (version might differ):
Client: Cloud integration: 1.0.17 Version: 20.10.8 API version: 1.41 ... Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12)
Validate Java.
java --version
Should display output like (version might differ):
openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.10+9) OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.10+9, mixed mode)
Download/clone the repo and change to the root folder:
git clone https://github.com/maeddes/options-galore-container-build
Note: Without git CLI you can download the repo as zip file here: https://github.com/maeddes/options-galore-container-build/archive/refs/heads/main.zip Extract it and change your command line shell to the root folder.
cd options-galore-container-build
Set to classic Docker mode:
export DOCKER_BUILDKIT=0
Build the code:
Change to the Java sample app
cd java
Option 1 (with local JDK installed)
./mvnw clean package
Option 2 (with local JDK installed - using docker)
docker run -it --rm --name my-maven-project -v "$(pwd)":/opt/app -w /opt/app maven:3.6.3-jdk-11 mvn clean install
Validate build artefact (timestamps will of course be different)
ls -ltr ./target/simplecode-0.0.1-SNAPSHOT.jar
-rw-r--r-- 1 mhs staff 18985578 Nov 26 10:24 ./target/simplecode-0.0.1-SNAPSHOT.jar
Observe contents of Dockerfile-simple-ubuntu
cat Dockerfile-simple-ubuntu
FROM ubuntu:20.10 RUN apt update && apt install openjdk-11-jre-headless -y COPY target/simplecode-0.0.1-SNAPSHOT.jar /opt/app.jar CMD ["java","-jar","/opt/app.jar"]
Build first image with this Dockerfile:
docker build -f Dockerfile-simple-ubuntu -t java-app:v-simple-ubuntu .
Build images with other predefined base images:
docker build -f Dockerfile-simple-adoptopenjdk -t java-app:v-simple-adoptopenjdk .
docker build -f Dockerfile-simple-openjdk -t java-app:v-simple-openjdk .
Validate images in local repo
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE java-app v-simple-openjdk 7209f28736c8 53 seconds ago 247MB java-app v-simple-adoptopenjdk 0859e7278963 About a minute ago 263MB java-app v-simple-ubuntu aed75c42f5d4 2 minutes ago 385MB openjdk 11-jre-slim 8bbc180a8a27 10 days ago 228MB adoptopenjdk 11-jre-hotspot 2c57fb3bc67b 8 weeks ago 244MB maven 3.6.3-jdk-11 e23b595c92ad 8 months ago 658MB
Observe build history and differences of the 3 images
docker history java-app:v-simple-ubuntu
docker history java-app:v-simple-adoptopenjdk
docker history java-app:v-simple-openjdk
You will observe different base layers and structure, but always the same top layer:
IMAGE CREATED CREATED BY SIZE COMMENT 7209f28736c8 3 minutes ago /bin/sh -c #(nop) CMD ["java" "-jar" "/opt/… 0B e5385e2e3146 3 minutes ago /bin/sh -c #(nop) COPY file:90a1db2252f31169… 19MB
Optional: Use tool "dive" to show detailed history of image:
dive java-app:v-simple-ubuntu dive java-app:v-simple-adoptopenjdk dive java-app:v-simple-openjdk
Use ctrl+m || ctrl+u
Build image with Multistage Dockerfile:
docker build -f Dockerfile-multistage-builder -t java-app:v-multistage-builder .
This will take a while as all the maven dependencies need to be downloaded.
Validate history:
docker history java-app:v-multistage-builder
Explore docker images:
docker images
The image with the tag <none> is the result of the first stage in the Dockerfile ("build").
REPOSITORY TAG IMAGE ID CREATED SIZE java-app v-multistage-builder ca155ffc6d2a 5 minutes ago 263MB <none> <none> 0498c7371461 5 minutes ago 739MB
Perform a slight modification in the source code which does not affect the behaviour of the application. You can use the editor 'nano' to execute this:
nano src/main/java/de/maeddes/simplecode/SimplecodeApplication.java
Locate the method hello()
@GetMapping("/") String hello(){ logger.info("Call to hello method on instance: " + getInstanceId()); return getInstanceId()+" Hello, Container people ! "; }
and just add some characters to the method name, e.g.
String helloABC(){
And save it using Ctrl+X and confirm with 'Y'.
docker images
Now you can repeat the docker build call.
docker build -f Dockerfile-multistage-builder -t java-app:v-multistage-builder .
You can observe that all the dependencies will need to get downloaded again. This method does not cache anything.
Change to new Docker mode:
export DOCKER_BUILDKIT=1
Observe changed output:
docker build -f Dockerfile-simple-adoptopenjdk -t java-app:v-simple-adoptopenjdk .
Obtain an alternative output:
docker build --progress=plain -f Dockerfile-simple-adoptopenjdk -t java-app:v-simple-adoptopenjdk .
Build with multistage experimental cache:
docker build -f Dockerfile-multistage-experimental-cache -t java-app:v-multistage-experimental-cache .
Change the code and rebuild:
You can use an editor to change a method name in src/main/java/de/maeddes/simplecode/SimplecodeApplication.java or simply execute
sed -i '' 's/hello/helloABC/g' src/main/java/de/maeddes/simplecode/SimplecodeApplication.java
(This is for sed under MacOS. For other Linux environments you might have to omit the '')
Rebuild and observe faster build through caching:
docker build -f Dockerfile-multistage-experimental-cache -t java-app:v-multistage-experimental-cache .
Observe the history to validate that top layer is still 'monolithic':
docker history java-app:v-multistage-experimental-cache
Build the code with a layered jar approach:
docker build -f Dockerfile-multistage-layered -t java-app:layered .
Display layered state
docker history java-app:layered
IMAGE CREATED CREATED BY SIZE COMMENT de2cb7c4be82 8 seconds ago ENTRYPOINT ["java" "org.springframework.boot… 0B buildkit.dockerfile.v0 <missing> 8 seconds ago COPY application/application/ ./ # buildkit 6.12kB buildkit.dockerfile.v0 <missing> 8 seconds ago COPY application/snapshot-dependencies/ ./ #… 0B buildkit.dockerfile.v0 <missing> 8 seconds ago COPY application/spring-boot-loader/ ./ # bu… 245kB buildkit.dockerfile.v0 <missing> 8 seconds ago COPY application/dependencies/ ./ # buildkit 18.9MB buildkit.dockerfile.v0
The following steps show how to build container images with the jib-maven plugin.
Again the use of the local maven wrapper (mvnw) will require a local JDK installation. If it’s not present use option 2.
Option 1:
./mvnw compile com.google.cloud.tools:jib-maven-plugin:3.2.1:dockerBuild -Dimage=java-app:jib
In this case the :dockerBuild part will instruct the plugin to build to the local docker daemon. The -Dimage parameter will specify the image name tag.
If you have a docker account you can login and push directly to the docker hub using: (Replace <docker_id> with your own username)
./mvnw compile com.google.cloud.tools:jib-maven-plugin:3.2.1:build -Dimage=<docker_id>/java-app:jib
Another option is to export the image directly to a tar. Use the following command.
./mvnw compile com.google.cloud.tools:jib-maven-plugin:3.2.1:buildTar -Dimage=java-app:jib
You will see an output saying
After that you can import the image into the local registry.
docker load -i target/jib-image.tar
15bbc04e2cf6: Loading layer [==================================================>] 41.71MB/41.71MB 7f270d883779: Loading layer [==================================================>] 16.82MB/16.82MB 496ff124a7de: Loading layer [==================================================>] 213B/213B 965a8d44c836: Loading layer [==================================================>] 1.345kB/1.345kB 5e91304a655b: Loading layer [==================================================>] 219B/219B Loaded image: java-app:jib
Option 2:
Without local maven you can only perform the tar build and direct import via load.
docker run -it --rm --name my-maven-project -v "$(pwd)":/opt/app -w /opt/app maven:3.6.3-jdk-11 mvn compile com.google.cloud.tools:jib-maven-plugin:3.2.1:buildTar -Dimage=java-app:jib
Load the exported tar file as image into the local registry.
docker load -i target/jib-image.tar
15bbc04e2cf6: Loading layer [==================================================>] 41.71MB/41.71MB 7f270d883779: Loading layer [==================================================>] 16.82MB/16.82MB 496ff124a7de: Loading layer [==================================================>] 213B/213B 965a8d44c836: Loading layer [==================================================>] 1.345kB/1.345kB 5e91304a655b: Loading layer [==================================================>] 219B/219B Loaded image: java-app:jib
Both options - final steps:
Now that you’ve built and loaded the image into the local registry using one of the options above, inspect the layered structure of the image.
docker history java-app:jib
IMAGE CREATED CREATED BY SIZE COMMENT bafe5ced0d6f 51 years ago jib-maven-plugin:3.1.4 82B jvm arg files <missing> 51 years ago jib-maven-plugin:3.1.4 2.37kB classes <missing> 51 years ago jib-maven-plugin:3.1.4 1B resources <missing> 51 years ago jib-maven-plugin:3.1.4 18.9MB dependencies
Optional: Perform some small modifications in the code similar to the ones during the Dockerfile exercise. Re-run the build steps and observe the caching and improved performance.
Note: All of the previous examples referenced the jib plugin directly in the maven call. An alternative (and probably the clean way) to the steps above is to add the plugin to your pom.xml:
The <to> tag in the following xml sets the target image path in the image registry. In our case we are using the local registry and thus just providing the image tag.
You can add the following plugin to your pom.xml
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.1.4</version>
<configuration>
<to>
<image>java-app:jib-v2.0</image>
</to>
</configuration>
</plugin>
In this case the invocation looks much simpler.
./mvnw compile jib:dockerBuild
The :build and :buildTar options work accordingly.
Access the pack CLI and list the suggest builders. A builder includes the buildpacks and environment that will be used for building and running your app.
pack builder suggest
Set a default builder to avoid specifying a builder every time you build. For the examples in this tutorial use the base builder image from Paketo buildpacks.
pack config default-builder paketobuildpacks/builder:base
Now all is set to build the container image using the buildpack. Simply execute:
pack build java-app:pack
The first invocation will take a long time. The builder image is big as it contains all the logic plus buildpacks.
After it is downloaded can now observe the output - the so-called bill of materials. This gives detailed information about the build.
Should display output like:
===> ANALYZING ... ===> DETECTING ... ===> RESTORING ===> BUILDING ... ===> EXPORTING ... Successfully built image java-app:pack^
Paketo buildpacks can be configured using different for external configuration (Environment Variables, buildpack.yml, Bindings, Procfiles).
Use an environment variable to configure the JVM version installed by the Java Buildpack and build a new version of the container image
source]
pack build java-app:pack-v2.0 --env BP_JVM_VERSION=8
Observe the usage of (JDK 8.0.332, JRE 8.0.332) in the BUILDING phase of the output.
Get an overview of the built Images
docker images
Using pack it is possible to swap out the underlying OS layers (run image) of an app image with another run image version, without re-building the application.
Rebase app image with a version pinned run image
pack rebase java-app:pack --run-image paketobuildpacks/run:1.3.48-full-cnb
Should display output like:
1.3.48-full-cnb: Pulling from paketobuildpacks/run 83525de54a98: Already exists c1dbbbd2a415: Pull complete 283105c565ee: Pull complete 7ead7caf102c: Pull complete Digest: sha256:005e54c4254bd49fa5b0b55fd7b7f16a2654bc6643963dece1cd03f7a0abce24 Status: Downloaded newer image for paketobuildpacks/run:1.3.48-full-cnb Rebasing java-app:pack on run image paketobuildpacks/run:1.3.48-full-cnb Saving java-app:pack... *** Images (a938edc476a8): java-app:pack Rebased Image: a938edc476a85ab53d6aa52a5cc6288c1dffdafd9b3654236cf8b62bbce70a83 Successfully rebased image java-app:pack
For a Spring Boot application you can also invoke Paketo Buildpacks directly via maven.
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=java-app:paketo
After compiling and testing the code within a standard Maven build, the build-image phase appears in the build log, in which you should observe display output like:
===> DETECTING ... ===> ANALYZING ... ===> RESTORING ===> BUILDING ... ===> EXPORTING ... Successfully built image 'docker.io/library/java-app:paketo'
Get an overview of the built Images
You have now completed the core exercise. Feel free to do some modifications yourself. Suggestions: * Edit the pom.xml and alternate the Java version (8,11,17 have been tested). * Do minor or major code modifications and observe changes * Use dive to analyze the created images.
© Matthias Haeussler. Free for private purposes. (Re)distribution for commercial purposes not allowed without owner permissions.