Deploy Spring Boot Apps using Kamal

Single command app deployments to bare-metal & VPS hosts.

Gunnar Hillert
6 min readOct 2, 2023
Taipei 101 tower, Taiwan

Did you ever want to easily deploy your smaller to medium sized Java applications to the cloud without the associated cost, be it the $€ cost or the cost of turning your laptop into a Kubernetes toaster?

David Heinemeier Hansson made a bit of a stir recently with his announcement Why we’re leaving the cloud. This was accompanied by a release announcement of Kamal 1.0. Kamal is really interesting as it allows you to deploy any dockerized application to bare-metal or plain cloud servers, while providing a super-simple user experience.

The best thing is that Kamal works also for Spring Boot applications. Do you remember the cf push experience back in the (public) Cloud Foundry days? Kamal provides kamal deploy. In this blog post, I would like to make a quick deployment of a simple Spring Boot application to a basic Linode (Akamai) instance. But you can literally deploy to any SSH-enabled machine that is able to run Docker. As such, you should be able apply the provided instructions below to alternative providers such as Hetzner et al.

Setting up Linode

First, let’s create 2 Linode instances. We can use the cheapest $5/month option: Nanode 1GB which provides 1GB of RAM and 1 CPU.

  • Create Linode labelled spring-boot-demo-1 ($5 / month)
  • Create Linode labelled spring-boot-demo-2 ($5 / month)
  • Create Linode Load Balancer (NodeBalancer) labelled spring-boot-demo-load-balancer ($10 / month)

The load balancer is certainly optional, but I would like to show how to deploy multiple instance of the application simultaneously. This way you can then also setup Cloudflare with one destination IP address. Nonetheless, if all you need is a single instance, a Load Balancer is not needed, reducing your total cost to just 5 bucks a month.

For the load balancer to work, you need to add a private IP address for each Linode. Also, make sure that under the Linode’s Configuration tab, the option Auto-configure networking is enabled. I experience an issue where the load balancer was unable to recognize that the instances were up and running.

Linode Configuration

With that in place you should be able to select each node when creating the load balancer:

Load balancer setup

Next, we will head over to start.spring.io to create a basic Spring Boot web application.

start.spring.io

Once, you downloaded the project, let’s add a web controller to your app:

@RestController
public class KamalController {

@GetMapping("/")
public String home() {
return "Hello Kamal!";
}
}

One more thing we need to add. We should enable the actuator health endpoint in the application, so that Kamal can check whether the application is up and running. Simply add the spring-boot-starter-actuator dependency to the pom.xml file:

  <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Now that that we set up the server environment and the application, we have to do the following steps to wire everything together:

  • Set up a Docker Registery to be able to push Docker images
  • Set up the Dockerfile for the Spring Boot application
  • Finish the Kamal configuration

Docker Registry

Kamal requires that you have push access to a Docker registry. As I already manage my code mostly on GitHub, I will use GitHub’s Container Registry to push Docker images of the Spring Boot application. Therefore, have your username and access token ready for Kamal further below. As a good preparation, set up the following environment variables and log into the docker registery.

export KAMAL_REGISTRY_USERNAME=you_github_username
export KAMAL_REGISTRY_PASSWORD=your_github_access_token
echo $KAMAL_REGISTRY_PASSWORD | docker login ghcr.io -u $KAMAL_REGISTRY_USERNAME --password-stdin

This should result in a message: Login Succeeded.

Dockerfile

Before we dive into Kamal, we should also make sure the Spring Boot application is ready for Docker. One important consideration in the context of Spring Boot is that Kamal does not support Buildpacks. This basically means: Dockerfiles are king! Let’s create a Dockerfile with the following contents:

FROM openjdk:21-alpine
RUN apk --no-cache add curl
EXPOSE 8080
ARG JAR_FILE=target/kamal-demo-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} kamal-demo.jar
ENTRYPOINT ["java","-jar","/kamal-demo.jar"]

It looks pretty straighforward, but please pay attention to line 2: We are installing curl. Kamal will use curl to check the Spring Boot application’s health actuator endpoint to determine whether the application is healthy or not. Let’s make sure that the Dockerfile works by building the application, creating the Docker image and running it on your local machine:

./mvnw clean package
docker build . -t kamal-demo/kamal-demo:0.0.1
docker run -it -p 8080:8080 kamal-demo/kamal-demo:0.0.1

The Docker container with the Spring Boot application should start up and under http://localhost:8080/ you should see:

Running Docker container

Setting up Kamal

Now we are ready to get going with Kamal. Make sure you install Kamal locally following the official instructions. With a pre-existing Ruby environment it should be as simple as:

gem install kamal

Now inside your Spring Boot appplication’s root directory initialize Kamal:

kamal init

This will add a few Kamal-related configuration files to your project. Of interest to us right now is config/deploy.yml and we make the following changes:

# Name of your application. Used to uniquely configure containers.
service: kamal-demo

# Name of the container image.
image: kamal-demo/demo

# Deploy to these servers.
servers:
- 123.123.123.123 # the public IP address of your first node
- 123.123.123.123 # the public IP address of your second node

## Credentials for your image host.
registry:
# Specify the registry server, if you're not using Docker Hub
server: ghcr.io
username:
- KAMAL_REGISTRY_USERNAME

# Always use an access token rather than real password when possible.
password:
- KAMAL_REGISTRY_PASSWORD

... #Other commented out stuff

# Configure a custom healthcheck (default is /up on port 3000)
healthcheck:
path: /actuator/health
port: 8080
max_attempts: 20
interval: 30s

Provide a name of your choosing for the service name, in this case kamal-demo. Specify the name of your Docker container image in the registry, e.g.: kamal-demo/demo. Next we need to provide the public IP addresses of our 2 Linode instances under servers. Under registry, specify the registry for your Docker images, in this case ghcr.io. The credentials will come from the 2 environment variables we create above:

  • KAMAL_REGISTRY_USERNAME
  • KAMAL_REGISTRY_PASSWORD

And lastly, we provide the healthcheck configuration pointing to our Spring Boot actator endpoint.

With all that in place, were are ready to roll by executing:

kamal setup

This command will, among other things, not only install Docker and Traefik onto your Linodes but will also push your app’s container to the registry, deploy the app to the Linodes and ensure that the Spring Boot application is running on both nodes. If everything runs successfully, you will be able to access the application on either both the Linode’s public IP addresses or via the Loadbalancer’s IP address.

Let’s change the Spring Boot web controller and change Hello Kamal! to Hello Spring Boot!. Next execute:

./mvn clean package
kamal deploy

This will build your Spring Boot application, deploy the update to your Linodes and a little later the application will respond with Hello Spring Boot!.

The response from the updated application

There are a few additional configuration options in the Kamal documentation such as performing rolling updates but I hope this gets you going and gives you the ability to make easy and affordable Spring Boot application deployments.

The source code used for this blog post is available at https://github.com/ghillert/kamal-demo

--

--

Gunnar Hillert

Consulting Member of Technical Staff at Oracle for the Coherence team. Java Champion, former Spring team member, OSS committer, DevNexus co-founder.