---
title: Notes on Creating a Docker Image for a Spring Boot App with CRaC Support
tags: ["Spring Boot", "Spring MVC", "Java", "CRaC"]
categories: ["Programming", "Java", "org", "springframework", "boot"]
date: 2023-12-01T10:45:18Z
updated: 2023-12-01T11:20:34Z
---

> ⚠️ This article was automatically translated by OpenAI (gpt-4o).
> It may be edited eventually, but please be aware that it may contain incorrect information at this time.

Spring Framework 6.1 / Spring Boot 3.2 has introduced initial support for [CRaC (Coordinated Restore at Checkpoint)](https://openjdk.org/projects/crac/).<br>
This is a note on creating a Docker image to try out CRaC.

> CRaC is also mentioned in the presentation [Virtual Threads! Checkpoint Restore! Introduction to Notable Features of Spring Framework 6.1 / Spring Boot 3.2](https://docs.google.com/presentation/d/17RdIx4ysrebxOTrGrBl2pr7yn9-T2krhyi9833C8pBE/edit#slide=id.p) given at JJUG CCC 2023 Fall.

First, create a template project.

```
curl https://start.spring.io/starter.zip \
  -s \
  -d type=maven-project \
  -d language=java \
  -d bootVersion=3.2.0 \
  -d baseDir=demo \
  -d groupId=com.example \
  -d artifactId=demo \
  -d name=demo \
  -d description=Demo \
  -d packageName=com.example.demo \
  -d packaging=jar \
  -d javaVersion=21 \
  -d dependencies=web,actuator \
  -o demo.zip
unzip demo.zip 
cd demo
git init
```

To add the CRaC API, add the following `<dependency>` to `pom.xml`. The version is managed by Spring Boot.

```xml
		<dependency>
			<groupId>org.crac</groupId>
			<artifactId>crac</artifactId>
		</dependency>
```

Add the following `HelloController`.

```java
cat <<EOF > ./src/main/java/com/example/demo/HelloController.java
package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

	@GetMapping(path = "/")
	public String hello() {
		return "Hello World!";
	}

}
EOF
```

Although not directly related to CRaC, to check the Java version information at the `/actuator/info` endpoint, add the following settings to `application.properties`.

```properties
cat <<EOF > ./src/main/resources/application.properties
management.endpoints.web.exposure.include=health,info
management.info.java.enabled=true
management.info.os.enabled=true
EOF
```

The `Dockerfile` is available on Gist. Since it may be updated, use the latest version. Refer to the following link for the implementation.

https://gist.github.com/making/35cfa52862e93793bad2b37b7c0e5135

```
wget https://gist.githubusercontent.com/making/35cfa52862e93793bad2b37b7c0e5135/raw/Dockerfile
wget https://gist.githubusercontent.com/making/35cfa52862e93793bad2b37b7c0e5135/raw/entrypoint.sh
```

The `Dockerfile` is written to build from the source code rather than from a jar (so it can also be used with Tanzu Application Platform).<br>
Therefore, even in an environment without JDK installed, you can build with the following command as long as Docker is available.

```
docker build -t demo .
```

Start the container with the following command. In this container, the app is started once, and then a Checkpoint is created. After the Checkpoint is created, the app will terminate. Then, the app is restored from the Checkpoint.<br>
To use CRaC, you need to grant some permissions with `--cap-add`. You can specify where to save the Checkpoint with the environment variable `CHECKPOINT_RESTORE_FILES_DIR`.

> On OrbStack, an error occurs when creating a Checkpoint, possibly because criu cannot be used.

```
docker run -p 8080:8080 \
  --cap-add CHECKPOINT_RESTORE \
  --cap-add NET_ADMIN \
  --cap-add SYS_PTRACE \
  --cap-add SYS_ADMIN \
  -v /tmp/crac:/var/crac \
  -e CHECKPOINT_RESTORE_FILES_DIR=/var/crac \
  --rm \
  demo
```

The following log will be output. The Checkpoint is created 10 seconds after the app starts. This time can be changed with the environment variable `SLEEP_BEFORE_CHECKPOINT` in `entrypoint.sh`.

```
Save checkpoint to /var/crac
Picked up JAVA_TOOL_OPTIONS:  -XX:+ExitOnOutOfMemoryError

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.0)

2023-12-01T07:47:31.527Z  INFO 10 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT using Java 21.0.1 with PID 10 (/application/BOOT-INF/classes started by root in /application)
2023-12-01T07:47:31.531Z  INFO 10 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to 1 default profile: "default"
2023-12-01T07:47:32.567Z  INFO 10 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2023-12-01T07:47:32.576Z  INFO 10 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-12-01T07:47:32.576Z  INFO 10 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.16]
2023-12-01T07:47:32.608Z  INFO 10 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-12-01T07:47:32.609Z  INFO 10 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 992 ms
2023-12-01T07:47:33.111Z  INFO 10 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 3 endpoint(s) beneath base path '/actuator'
2023-12-01T07:47:33.169Z  INFO 10 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2023-12-01T07:47:33.181Z  INFO 10 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.067 seconds (process running for 2.355)
Picked up JAVA_TOOL_OPTIONS:  -XX:+ExitOnOutOfMemoryError
10:
2023-12-01T07:47:41.076Z  INFO 10 --- [Attach Listener] jdk.crac                                 : Starting checkpoint
CR: Checkpoint ...
/application/entrypoint.sh: line 18:    10 Killed                  java -XX:CRaCCheckpointTo=$CHECKPOINT_RESTORE_FILES_DIR org.springframework.boot.loader.launch.JarLauncher
2023-12-01T07:47:44.605Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Restarting Spring-managed lifecycle beans after JVM restore
2023-12-01T07:47:44.609Z  INFO 10 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2023-12-01T07:47:44.610Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Spring-managed lifecycle restart completed (restored JVM running for 35 ms)
```

You can access the app normally.

```
$ curl localhost:8080
Hello World!
```

When you access `/actuator/info`, you can check the Java information. The `vendor.version` includes "CRaC-CA", indicating that it is a Java version compatible with CRaC.

```
$ curl -s localhost:8080/actuator/info | jq .
{
  "java": {
    "version": "21.0.1",
    "vendor": {
      "name": "Azul Systems, Inc.",
      "version": "Zulu21.30+23-CRaC-CA"
    },
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "21.0.1+12-LTS"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Azul Systems, Inc.",
      "version": "21.0.1+12-LTS"
    }
  },
  "os": {
    "name": "Linux",
    "version": "5.15.0-89-generic",
    "arch": "amd64"
  }
}
```

When you check the directory specified by `CHECKPOINT_RESTORE_FILES_DIR`, the following files are created.

```
$ ls -lh /tmp/crac/
total 184M
-rw-r--r-- 1 root root 2.2K Dec  1 04:46 core-10.img
-rw-r--r-- 1 root root  795 Dec  1 04:46 core-12.img
-rw-r--r-- 1 root root  767 Dec  1 04:46 core-13.img
...
-rw-r--r-- 1 root root  795 Dec  1 04:46 core-89.img
-rw-r--r-- 1 root root  790 Dec  1 04:46 core-90.img
-rw-r--r-- 1 root root  782 Dec  1 04:46 core-91.img
-rw------- 1 root root 380K Dec  1 04:46 dump4.log
-rw-r--r-- 1 root root  524 Dec  1 04:46 fdinfo-2.img
-rw-r--r-- 1 root root 6.3K Dec  1 04:46 files.img
-rw-r--r-- 1 root root   18 Dec  1 04:46 fs-10.img
-rw-r--r-- 1 root root   36 Dec  1 04:46 ids-10.img
-rw-r--r-- 1 root root   46 Dec  1 04:46 inventory.img
-rw-r--r-- 1 root root  12K Dec  1 04:46 mm-10.img
-rw-r--r-- 1 root root 7.1K Dec  1 04:46 pagemap-10.img
-rw-r--r-- 1 root root 183M Dec  1 04:46 pages-1.img
-rw-r--r-- 1 root root   98 Dec  1 04:46 pstree.img
-rw-r--r-- 1 root root   12 Dec  1 04:46 seccomp.img
-rw-r--r-- 1 root root   54 Dec  1 04:46 stats-dump
-rw-r--r-- 1 root root   34 Dec  1 04:46 timens-0.img
```

Stop the container with Ctrl+C.

Start the container again with the following command.

```
docker run -p 8080:8080 \
  --cap-add CHECKPOINT_RESTORE \
  --cap-add NET_ADMIN \
  --cap-add SYS_PTRACE \
  --cap-add SYS_ADMIN \
  -v /tmp/crac:/var/crac \
  -e CHECKPOINT_RESTORE_FILES_DIR=/var/crac \
  --rm \
  demo
```

The following log will be output. Since it was restored from the Checkpoint, it started in 30ms.

```
Restore checkpoint from /var/crac
2023-12-01T07:53:41.374Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Restarting Spring-managed lifecycle beans after JVM restore
2023-12-01T07:53:41.379Z  INFO 10 --- [Attach Listener] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 8080 (http) with context path ''
2023-12-01T07:53:41.381Z  INFO 10 --- [Attach Listener] o.s.c.support.DefaultLifecycleProcessor  : Spring-managed lifecycle restart completed (restored JVM running for 27 ms)
```

You can access the app.

```
$ curl localhost:8080
Hello World!
```

As long as you use the same Checkpoint, changes to the code will not be reflected in the app. In the above `entrypoint.sh`, if you set the environment variable `CLEAN_CHECKPOINT` to `true`, the folder will be cleaned once and the Checkpoint will be recreated.

Since the Checkpoint needs to be recreated when the code changes, in practice, `CHECKPOINT_RESTORE_FILES_DIR` should be dynamically set to correspond to the revision.

For samples using other Spring features, refer to
https://github.com/spring-projects/spring-checkpoint-restore-smoke-tests.
If you use HikariCP, you need to set `spring.datasource.hikari.allow-pool-suspension=true`, among other considerations.
