Deploying Hilla apps as native image

René Wilby | Jun 17, 2024 min read

Build Hilla app as native image for deployment

A Hilla app that was created using the Hilla CLI, can be build as a native image for deployment using Maven (or Gradle). This requires a JDK with GraalVM support. A Hilla app built as a native image is a stand-alone executable that can be executed without a Java runtime environment. Compared to a Hilla app built and executed as a JAR file, a Hilla app built and executed as a native image is usually characterized by a lower startup time and memory usage. A Hilla app can be built as a native image with the support of GraalVM as follows:

./mvnw clean package -Pproduction -Pnative native:compile

The native image created contains all the necessary classes, dependencies and the frontend bundle. As a Hilla app is a classic Spring Boot project, the native image created also contains an integrated server that serves the Hilla app as soon as the native image is executed.

Further information on Spring Boot’s support for native images can be found here.

Execute Hilla app as native image

The built Hilla app my-app can be executed as a native image as follows:

./target/my-app

Spring Boot specific configurations can be set or overridden as environment variables if required. For example, it is possible to adjust the log level at the start of the application as follows:

export LOGGING_LEVEL_COM_VAADIN=DEBUG && ./target/my-app

Create Docker image for Hilla app

Applications are often deployed based on containers. It is therefore advisable to create a Dockerfile for the Hilla app. The Dockerfile should be located in the base directory of the Hilla app and have the following content:

FROM debian:bookworm-slim

WORKDIR /app

COPY target/*.so /app/
COPY target/my-app /app/my-app

EXPOSE 8080

CMD ["/app/my-app"]

The Dockerfile is based on a slimmed-down base image, here debian:bookworm-slim. The native image of the Hilla app is copied into the Docker image together with the libraries (.so files) created during the build. The libraries provide dependencies that are not part of the native image but are required by the application during execution. Furthermore, the Dockerfile contains the configuration for the port via which the application can receive requests. The CMD is used to specify that the native image in the Docker image should be executed when the container is started.

Create Docker image locally

The Docker image can be created locally with Docker or Podman:

docker|podman build --tag my-app .

The command is executed in the base directory of the Hilla app. The . points to the same directory and the Dockerfile in it.

The Docker image is then available and usable locally.

Run Docker container locally

A Docker container with the Hilla app can be started based on the Docker image created:

docker|podman run -it --rm --publish 8080:8080 my-app

Extends Dockerfile with build stage

Building the Hilla app as a native image can also be part of the Dockerfile. For this purpose, the Dockerfile is divided into two stages:

# First stage: JDK with GraalVM and native image support
FROM ghcr.io/graalvm/native-image-community:21.0.2 AS build

WORKDIR /usr/src/app

# Copy project files 
COPY . .

# Create native image
RUN ./mvnw clean package -Pproduction -Pnative native:compile

# Second stage: Lightweight debian-slim image
FROM debian:bookworm-slim

WORKDIR /app

# Copy the produced library files from the build stage
COPY --from=build /usr/src/app/target/*.so /app/
# Copy the native image from the build stage
COPY --from=build /usr/src/app/target/my-app /app/my-app

EXPOSE 8080

# Run the application
CMD ["/app/my-app"]

The native image is created in the first stage of the Dockerfile. To do this, the required files and dependencies are copied into the Docker image, and then the Hilla app is built as a native image. In the second stage, the Docker image is created with the built native image.

Deploy Hilla app to fly.io

With the help of the Dockerfile created, a Hilla app can be deployed very easily via a service such as fly.io. The registration at fly.io is free, but requires credit card details. Fly.io offers a free hobby plan, which allows you to deploy applications with limited resources free of charge. The app can then be accessed via https://<unique-app-identifier>.fly.dev. The necessary configuration of IP addresses, DNS, SSL certificate etc. is handled by fly.io.

The path to deployment is described in detail in https://fly.io/docs/hands-on/.

First, the flyctl is installed. Under macOS, this can be done with brew, for example:

brew install flyctl

Afterwards, a new account is created with fly.io or an existing account is linked:

fly auth signup

A deployment configuration for fly.io is created before the first deployment. This is done using the command:

fly launch --dockerfile Dockerfile

The suggested settings can be adopted. A .dockerignore should not be created initially, as this would prevent the native image and the libraries from being copied for the time being.

flyctl stores the deployment configuration in the file fly.toml. This file can be edited if required.

app = '<unique-app-identifier>'
primary_region = 'ams'

[build]
  dockerfile = 'Dockerfile'

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0
  processes = ['app']

[[vm]]
  size = 'shared-cpu-1x'
  memory = '256mb'

The actual deployment of the Hilla app is then carried out using:

fly deploy

The Dockerfile, the native image and libraries (.so files) are uploaded to a build server of fly.io. The Docker image is created at fly.io. Fly.io stores the created Docker image in its own registry and then starts the Hilla app as a Docker container. After a successful start, the app can be called up in the browser via https://<unique-app-identifier>.fly.dev.

If you want to use the Dockerfile with the two stages as shown above, please note that the available resources of the Hobby plan will probably not be sufficient and the build process of the Docker image at fly.io may therefore fail.

Update Hilla app

If changes are made to the code of the Hilla app, it must be rebuilt and then deployed again:

./mvnw clean package -Pproduction -Pnative native:compile
fly deploy

If the native image is built via a separate stage in the Dockerfile, only the execution of fly deploy is required.

Summary

A Hilla app can be built very easily for deployment as a native image with the support of GraalVM. With the help of a suitable Dockerfile, the deployment via a service such as fly.io is not a problem. The official Hilla documentation offers further instructions for deploying Hilla apps, e.g. to AWS, GCP, Azure or Heroku.

The Hilla blog also contains a blog post by Marcus Hellberg on this topic: Deploying a Spring Boot app as a native GraalVM image with Docker.

An example Hilla app that was build as native image and deployed to fly.io can be accessed via https://vaadin-create-countdown.fly.dev/. The corresponding source code is available at GitHub.