Build a rock for a Spring Boot app

In this tutorial, we’ll containerise a simple Spring Boot app into a rock using Rockcraft’s spring-boot-framework extension.

It should take 25 minutes for you to complete.

You won’t need to come prepared with intricate knowledge of software packaging, but familiarity with Linux paradigms, terminal operations, and Spring Boot is required.

Once you complete this tutorial, you’ll have a working rock for a Spring Boot app. You’ll gain familiarity with Rockcraft and the spring-boot-framework extension, and have the experience to create rocks for Spring Boot apps.

Setup

We recommend starting from a clean Ubuntu installation. If we don’t have one available, we can create one using Multipass:

Is Multipass already installed and active? Check by running

snap services multipass

If we see the multipass service but it isn’t “active”, then we’ll need to run sudo snap start multipass. On the other hand, if we get an error saying snap "multipass" not found, then we must install Multipass:

sudo snap install multipass

Then we can create the VM with the following command:

multipass launch --disk 10G --name rock-dev 24.04

Finally, once the VM is up, open a shell into it:

multipass shell rock-dev

LXD will be required for building the rock. Make sure it is installed and initialised:

sudo snap install lxd
lxd init --auto

In order to create the rock, we’ll install Rockcraft with classic confinement, which grants it access to the whole file system:

sudo snap install rockcraft --classic --channel latest/edge

This tutorial requires the latest/edge channel of Rockcraft as the framework is currently experimental.

We’ll use Docker to run the rock. We can install it as a snap:

sudo snap install docker

By default, Docker is only accessible with root privileges (sudo). We want to be able to use Docker commands as a regular user:

sudo addgroup --system docker
sudo adduser $USER docker
newgrp docker

Restart Docker:

sudo snap disable docker
sudo snap enable docker

Note that we’ll also need a text editor. We can either install one of our choice or simply use one of the already existing editors in the Ubuntu environment (like vi).

In order to test the Spring Boot app locally, before packing it into a rock, install devpack-for-spring and Java.

sudo snap install devpack-for-spring --classic
sudo apt update && sudo apt install -y openjdk-21-jdk

Create the Spring Boot app

Start by creating the “Hello, world” Spring Boot app that will be used for this tutorial.

Create an empty project directory:

mkdir spring-boot-hello-world
cd spring-boot-hello-world

Create the Demo Spring Boot app that will be used for this tutorial.

devpack-for-spring boot start \
  --path maven-app \
  --project maven-project \
  --language java \
  --boot-version 3.4.4 \
  --version 0.0.1 \
  --group com.example \
  --artifact demo \
  --name demo \
  --description "Demo project for Spring Boot" \
  --package-name com.example.demo \
  --dependencies web \
  --packaging jar \
  --java-version 21
cd maven-app

Build the Spring Boot app so it can be run:

./mvnw clean install

A jar called demo-0.0.1.jar is created in the target directory. This jar is only needed for local testing, as Rockcraft will package the Spring Boot app when we pack the rock.

Let’s Run the Spring Boot app to verify that it works:

java -jar target/demo-0.0.1.jar

The app starts an HTTP server listening on port 8080 that we can test by using curl to send a request to the root endpoint. We may need a new terminal for this – run multipass shell rock-dev to get another terminal:

curl localhost:8080

The Spring Boot app should respond with {"timestamp":<timestamp>,"status":404,"error":"Not Found","path":"/"}.

The Spring Boot app looks good, so let’s stop it for now with Ctrl + C.

Pack the Spring Boot app into a rock

Now let’s create a container image for our Spring Boot app. We’ll use a rock, which is an OCI-compliant container image based on Ubuntu.

First, we’ll need a rockcraft.yaml project file. We’ll take advantage of a pre-defined extension in Rockcraft with the --profile flag that caters initial rock files for specific web app frameworks. Using the Spring Boot profile, Rockcraft automates the creation of rockcraft.yaml and tailors the file for a Spring Boot app. From the ~/spring-boot-hello-world directory, initialize the rock:

rockcraft init --profile spring-boot-framework

The rockcraft.yaml file will automatically be created and set the name based on your working directory.

Check out the contents of rockcraft.yaml:

cat rockcraft.yaml

The top of the file should look similar to the following snippet:

~/spring-boot-hello-world/rockcraft.yaml
name: spring-boot-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, a ubuntu base can be used
build-base: [email protected] # build-base is required when the base is bare
version: '0.1' # just for humans. Semantic versioning is recommended
summary: A summary of your Spring Boot application # 79 char long summary
description: |
    This is spring-boot-hello-world's description. You have a paragraph or two to tell the
    most important story about it. Keep it under 100 words though,
    we live in tweetspace and your description wants to look good in the
    container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
    amd64:
    # arm64:
    # ppc64el:
    # s390x:

Verfiy that the name is spring-boot-hello-world.

The platforms key must match the architecture of your host. Check the architecture of your system:

dpkg --print-architecture

Edit the platforms key in rockcraft.yaml if required.

Note

For this tutorial, we name the rock spring-boot-hello-world and assume we are running on an amd64 platform. Check the architecture of the system using dpkg --print-architecture.

The name, version and platform all influence the name of the generated .rock file.

As the spring-boot-framework extension is still experimental, export the environment variable ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS:

export ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true

Pack the rock:

rockcraft pack

Warning

There is a known connectivity issue with LXD and Docker. If we see a networking issue such as “A network related operation failed in a context of no network access” or Client.Timeout, allow egress network traffic to flow from the LXD managed bridge using:

iptables  -I DOCKER-USER -i <network_bridge> -j ACCEPT
ip6tables -I DOCKER-USER -i <network_bridge> -j ACCEPT
iptables  -I DOCKER-USER -o <network_bridge> -m conntrack \
  --ctstate RELATED,ESTABLISHED -j ACCEPT
ip6tables -I DOCKER-USER -o <network_bridge> -m conntrack \
  --ctstate RELATED,ESTABLISHED -j ACCEPT

Run lxc network list to show the existing LXD managed bridges.

Depending on the network, this step can take a couple of minutes to finish.

Once Rockcraft has finished packing the Spring Boot rock, we’ll find a new file in the working directory (an OCI image) with the .rock extension:

ls *.rock -lh

Run the Spring Boot rock with Docker

We already have the rock as an OCI archive. Now we need to load it into Docker. Docker requires rocks to be imported into the daemon since they can’t be run directly like an executable.

Copy the rock:

rockcraft.skopeo copy \
  --insecure-policy \
  oci-archive:spring-boot-hello-world_0.1_$(dpkg --print-architecture).rock \
  docker-daemon:spring-boot-hello-world:0.1

This command contains the following pieces:

  • --insecure-policy: adopts a permissive policy that removes the need for a dedicated policy file.

  • oci-archive: specifies the rock we created for our Spring Boot app.

  • docker-daemon: specifies the name of the image in the Docker registry.

Check that the image was successfully loaded into Docker:

docker images spring-boot-hello-world:0.1

The output should list the Spring Boot container image, along with its tag, ID and size:

REPOSITORY       TAG       IMAGE ID       CREATED         SIZEspring-boot-hello-world   0.1       f3abf7ebc169   5 minutes aspring-boot   15.7MB

Now we’re finally ready to run the rock and test the containerised Spring Boot app:

docker run --rm -d -p 8080:8080 \
  --name spring-boot-hello-world spring-boot-hello-world:0.1

Use the same curl command as before to send a request to the Spring Boot app’s root endpoint which is running inside the container:

curl localhost:8080

The Spring Boot app again responds with {"timestamp":<timestamp>,"status":404,"error":"Not Found","path":"/"}.

View the app logs

When deploying the Spring Boot rock, we can always get the app logs with Pebble:

docker exec spring-boot-hello-world pebble logs spring-boot

As a result, Pebble will give the logs for the spring-boot service running inside the container. We should expect to see something similar to this:

.   ____          _            __ _ _/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \\\/  ___)| |_)| | | | | || (_| |  ) ) ) )'  |____| .__|_| |_|_| |_\__, | / / / /=========|_|==============|___/=/_/_/_/ :: Spring Boot ::                (v3.4.4) 2025-04-09T03:48:07.704Z  INFO 4258 --- [demo] [main] com.example.demo.DemoApplication:Starting DemoApplication v0.0.1 using Java 21.0.6 with PID <redacted>.

We can also choose to follow the logs by using the -f option with the pebble logs command above. To stop following the logs, press Ctrl + C.

Stop the app

Now we have a fully functional rock for a Spring Boot app! This concludes the first part of this tutorial, so we’ll stop the container and remove the respective image for now:

docker stop spring-boot-hello-world
docker rmi spring-boot-hello-world:0.1

Update the Spring Boot app

As a final step, let’s update our app. For example, we want to add a new /time endpoint which returns the current time.

Start by creating the src/main/java/com/example/demo/TimeController.java file in a text editor and paste in the code to look like the following:

~/spring-boot-hello-world/src/main/java/com/example/demo/ TimeController.java
package com.example.demo;

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

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;

@RestController
public class TimeController {

    @GetMapping("/time")
    public Map<String, String> getCurrentTime() {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
        String timestamp = now.format(formatter);

        Map<String, String> response = new HashMap<>();
        response.put("timestamp", timestamp);
        return response;
    }
}

Since we are creating a new version of the app, open the project file and set version: '0.2'. The top of the rockcraft.yaml file should look similar to the following:

~/spring-boot-hello-world/rockcraft.yaml
name: spring-boot-hello-world
# see https://documentation.ubuntu.com/rockcraft/en/latest/explanation/bases/
# for more information about bases and using 'bare' bases for chiselled rocks
base: bare # as an alternative, a ubuntu base can be used
build-base: [email protected] # build-base is required when the base is bare
version: '0.2'
summary: A summary of your Spring Boot application # 79 char long summary
description: |
    This is spring-boot-hello-world's description. You have a paragraph or two to tell the
    most important story about it. Keep it under 100 words though,
    we live in tweetspace and your description wants to look good in the
    container registries out there.
# the platforms this rock should be built on and run on.
# you can check your architecture with `dpkg --print-architecture`
platforms:
    amd64:
    # arm64:
    # ppc64el:
    # s390x:

Note

rockcraft pack will create a new image with the updated code even if we don’t change the version. It is recommended to change the version whenever we make changes to the app in the image.

Pack and run the rock using similar commands as before:

ls -lah
rockcraft pack
rockcraft.skopeo --insecure-policy \
  copy oci-archive:spring-boot-hello-world_0.2_$(dpkg --print-architecture).rock \
  docker-daemon:spring-boot-hello-world:0.2
docker images spring-boot-hello-world:0.2
docker run --rm -d -p 8080:8080 \
  --name spring-boot-hello-world spring-boot-hello-world:0.2

Note

Note that the resulting .rock file will now be named differently, as its new version will be part of the filename.

Finally, use curl to send a request to the /time endpoint:

curl --fail localhost:8080/time

The updated app will respond with the current date and time.

Note

If we are not getting the current date and time from the /time endpoint, check the Troubleshooting steps below.

Cleanup

We can now stop the container and remove the corresponding image:

docker stop spring-boot-hello-world
docker rmi spring-boot-hello-world:0.2

Reset the environment

We’ve reached the end of this tutorial.

If we’d like to reset the working environment, we can simply run the following:

# delete all the files created during the tutorial
cd .. && rm -rf maven-app

We can also clean the Multipass instance up. Start by exiting it:

exit

And then we can proceed with its deletion:

multipass delete rock-dev
multipass purge

Next steps


Troubleshooting

App updates not taking effect?

Upon changing the Spring Boot app and re-packing the rock, if the changes are not taking effect, try running rockcraft clean and pack the rock again with rockcraft pack.