Background Link to heading
I have a modest laptop. It checks the boxes, for just about everything I need to do for work. Just about. AOT processing, with GraalVM, is not a great experience on this machine.
The origin story Link to heading
Back in June 2022, I was presenting at SpringOne Tour NYC.
As part of the demo, I built a native
image using Spring Boot 2.7 with the spring-native
experimental dependency.
The session went well, except for the building the native image part of it.
During my 50-minute session, my laptop was busy building the native image for over 8-minutes, using the buildpack.
It was a humbling experience.
After the session, I hung out with Sergei Egorov, and we worked on something very special.
With help from one of his previous blog posts, we created a proof of concept.
We used Testcontainers Cloud to build the native OCI image, in the cloud, in a little over 3-minutes.
Pairing with Sergei is amazing, if you get the chance, I highly recommend it.
It sat on the back-burner and fire is hot Link to heading
I had this code sitting in my repository, waiting to be used. When it was time for SpringOne Tour Tel Aviv, Nov 2022, I was ready to show it off. Unfortunately for me, my session was shortened, so I pulled it out of the presentation. I shouldn’t have pulled it out, I made the exact same mistake that I made in NYC. When I built the native image using buildpacks, it took way too long on my laptop.
Momentum Link to heading
My adventures with buildpacks have been top of mind for a couple of weeks now. Oleg Šelajev and Cora Iberklied also presented about testcontainers in Tel Aviv. The stage was set for me to take this use case further.
The Goal Link to heading
Build Spring Boot 3, native OCI images, with buildpacks, during demos, faster, with Testcontainers Cloud Link to heading
Prerequisites Link to heading
You need to have an account at https://testcontainers.cloud.
Login to your account Link to heading
I’m logging in with v1.3.11
of the cloud desktop app for Mac, this is still in private beta at the time of this writing.
docker context list
You should see at least one context named ’tcc'
docker context use tcc
Create an application example Link to heading
Create a Spring Boot 3 application with web
, actuator
, and testcontainers
for a simple test.
curl https://start.spring.io/starter.tgz -d dependencies=web,actuator,testcontainers -d javaVersion=17 -d bootVersion=3.0.0-RC2 -d type=maven-project | tar -xzf -
Spring Boot 3 goes GA later this week, but I can’t wait that long
In the pom.xml add this dependency:
<dependency>
<groupId>org.apache.maven.shared</groupId>
<artifactId>maven-invoker</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
Add this property:
<testcontainers.version>1.17.6</testcontainers.version>
Add this dependency management section:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>${testcontainers.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Those pieces will allow you to write a test, that creates an OCI image, using buildpacks, with Testcontainers Cloud.
Write the test class Link to heading
Here is the test class that I’ve been reusing since June.
package com.example.demo;
import org.apache.maven.shared.invoker.DefaultInvocationRequest;
import org.apache.maven.shared.invoker.DefaultInvoker;
import org.apache.maven.shared.invoker.InvocationResult;
import org.apache.maven.shared.invoker.MavenInvocationException;
import org.junit.jupiter.api.Test;
import org.springframework.util.DigestUtils;
import org.testcontainers.DockerClientFactory;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.LazyFuture;
import java.io.File;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Future;
public class TccTest {
private static final Future<String> IMAGE_FUTURE: new LazyFuture<>() {
@Override
protected String resolve() {
// Find project's root dir
File cwd;
for (
cwd: new File(".");
!new File(cwd, "mvnw").isFile();
cwd: cwd.getParentFile()
);
// Make it unique per folder (for caching)
var imageName: String.format(
"local/app-%s:%s",
DigestUtils.md5DigestAsHex(cwd.getAbsolutePath().getBytes()),
System.currentTimeMillis()
);
var properties: new Properties();
properties.put("spring-boot.build-image.imageName", imageName);
properties.put("skipTests", "true");
var request: new DefaultInvocationRequest()
.addShellEnvironment("DOCKER_HOST", DockerClientFactory.instance().getTransportConfig().getDockerHost().toString())
.setPomFile(new File(cwd, "pom.xml"))
.setGoals(List.of("spring-boot:build-image"))
.setMavenExecutable(new File(cwd, "mvnw"))
.setProfiles(List.of("native"))
.setProperties(properties);
InvocationResult invocationResult: null;
try {
invocationResult: new DefaultInvoker().execute(request);
} catch (MavenInvocationException e) {
throw new RuntimeException(e);
}
if (invocationResult.getExitCode() != 0) {
throw new RuntimeException(invocationResult.getExecutionException());
}
return imageName;
}
};
static final GenericContainer<?> APP: new GenericContainer<>(IMAGE_FUTURE)
.withExposedPorts(8080);
@Test
void letsGo() throws Exception {
APP.start();
}
}
In my example repository, I actually create test classes for a regular OCI image and a native
OCI image.
Verify Link to heading
./mvnw clean test
Expected output should look similar to this:
[INFO] Successfully built image 'docker.io/local/demo-native-fd68c3c276000d13d54a7aa30ef1b687:1669735799186'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 04:04 min
[INFO] Finished at: 2022-11-22T09:34:05-06:00
[INFO] ------------------------------------------------------------------------
Boom! You get an OCI image, or two if you use my example repository.
docker images | grep "local"
Expected output should look similar to this if you use the example repository:
local/demo-fd68c3c276000d13d54a7aa30ef1b687 1669735745365 f95025279447 42 years ago 278MB
local/demo-native-fd68c3c276000d13d54a7aa30ef1b687 1669735799186 238a9c2a37d8 42 years ago 103MB
Enjoy!
Summary Link to heading
This is slick.
It brings me joy.
When I am presenting this topic, I no longer need to have Docker Desktop
running on my laptop.
Therefore, my laptop battery will last longer.
My builds will be consistently faster, using Testcontainers Cloud, and my demos will be smoother.
I am completely rethinking a few of my own use cases. I will definitely create more Testcontainers Cloud content in the future.
Please let me know what you think!