Sitemap
Jmix

Jmix

Jmix accelerates full-stack development for Java devs by combining Spring Boot and Vaadin into an intuitive, efficient framework for building scalable web apps

Well-done Java Containers: Simplifying Web App Development

11 min readSep 15, 2024

--

One common challenge developers face when creating business systems is efficiently handling front-end and back-end integration, especially with Java-based frameworks. Traditional approaches often involve managing multiple technologies, leading to slower development and higher costs.

In this article, we aim to explore how Jmix addresses these complexities by providing an all-in-one solution that integrates key technologies like Spring and Vaadin. We will walk through the main issues developers face, such as managing data models and UI components, and demonstrate how Jmix streamlines these processes, offering flexibility without sacrificing productivity. Whether you’re building a full-stack web app or a REST API backend, Jmix’s toolkit makes the process much smoother and more efficient.

When you start developing a web application, normally, you can run it on a local computer or in some development environment with just a few clicks. However, modern frameworks are often divided into “backend”/”frontend” categories, which means their source code is running on separate scopes. Technology trends also force software engineers to divide front and back-end parts into highly coupled domain-based units called micro-services or micro-frontends. These complexities make it difficult to view and develop the application as a single unit, slowing the development process and requiring many specialists and a large infrastructure, which can be costly for your company or project. But what if you need a simple app to solve your concrete business objectives?

The most popular out-of-box-ready solution for the Java world is Spring Boot framework, which probably has “batteries” for every web development purpose. But just using Spring Boot doesn’t give you the feeling of a complete solution. You must decide how your application will present user interfaces and then manage technological and compatibility issues between different parts. Even projects like JHipster, which use a single control interface to manage builds, only hide the issue by using code automation tools.

Some technologies are attempting to provide integrated frontend-to-backend solutions. One of them, named Vaadin framework, allows you to write entire web applications using Java only. This sounds great for developers who don’t want to dive into the front-end technology swamp, but this development feels too code-oriented. Using it, you will have to instantiate many boring component classes and configure each one with tons of boilerplate code. This makes the result code-first solutions less extensible than traditional web technologies as it doesn’t give the same level of flexibility and extensibility as with markup and dynamic languages.

The Jmix platform incorporates an open-source framework that leverages Spring Boot, Vaadin, and other essential development technologies. While most web projects nowadays are developed as standalone but highly coupled frontend and backend applications, just like Vaadin Jmix empowers you to develop complete web applications with Java code but with the ability to define layouts in a special XML markup. Jmix can be used as a full-stack solution or as a REST-backend for your frontend project, so in this article, we will try to observe how to pack your Jmix or Spring Boot project into a Docker container.

The best development performance can be gained after you install the Jmix plugin into your IntelliJ IDEA.

After creating the Jmix project to run your application you don’t need to install any additional modules/plugins or do any additional setup. However, having it running on production servers may still require some tweaks.

In case you are already familiar with the Spring Boot ecosystem, all the examples in this article will be directly applicable to your projects.

We will start with a ready-to-use demo project called “Jmix Onboarding”. It already has some code stuff to observe.

You can get it from the following GitHub repository.

Just select in the main menu File -> New -> Project from Version Control…

Use the repository URL above to load the project.

Spring Boot project tooling provides an out-of-the-box solution for container building. These tools are based on Paketo Buildpacks (or Cloud Native Buildpacks, aka CNB), offering simplicity and a wealth of useful features.

To build a Jmix project from the console, just run the following command:

./gradlew bootBuildImage --imageName=onboarding

To build a Vaadin/Jmix-based project container, you will also have to add the argument Pvaadin.productionMode=true. It switches the tooling into optimized building mode and it is required to run the project in a container.

./gradlew -Pvaadin.productionMode=true bootBuildImage --imageName=onboarding

So, we built our first container image, but you need to make some improvements to have it running well on your production servers.

Prepare the project for running in a container

Generally, we can develop and run projects from the IDE just with play/debug buttons. It will use a file-based local database HSQL that may be fine for development, but for a container environment, it is recommended to use server database systems like PostgreSQL, MySQL/MariaDB, and many others. Jmix allows us to support all these databases in the same project, but to achieve this, you should enable it in Liquibase migrations generation settings.

Press enter or click to view image in full size
With Jmix, you can support multiple databases within the same project. Simply enable it in the Liquibase migration generation settings.

This will make our project database vendor agnostic so we can develop it with a local Postgres db and deploy it to a server database system almost effortlessly.

By enabling this, you can forget about manually writing database scripts and updating executions, but you can still do that in case you have special needs. Whenever you change your entities and rerun the development server, the Jmix Studio Plugin will automatically generate Liquibase scripts compatible with the chosen database when possible!

Spring Boot profiles

To have different database and application configurations for development and production, we will use Spring Boot profiles environment.

To continue development with the local HSQL database, we will move its data source configuration from the application.properties configuration file to a special development-scoped application-dev.properties file:

main.datasource.url = jdbc:hsqldb:file:.jmix/hsqldb/onboarding 
main.datasource.username = sa
main.datasource.password =

For production use, we will create a new application-prod.properties file and add a database connection configuration filled with variables:

main.datasource.url = jdbc:${DB_VENDOR}://${DB_HOST}:${DB_PORT}/${DB_NAME} 
main.datasource.username=${DB_USER}
main.datasource.password=${DB_PASSWORD}

jmix.localfs.storage-dir = /storage

This will allow us to change database settings without rebuilding and redeploying the entire project.

To enable Spring Boot profile switching add profile activation properties into build.gradle file.

bootRun { 
args = ["--spring.profiles.active=dev"]
}

tasks.named("bootBuildImage") {
environment["BPE_APPEND_JAVA_TOOL_OPTIONS"] = " -Dspring.profiles.active=prod"
}

As an alternative to configurations profile can be switched on at boot time:

docker run -e "SPRING_PROFILES_ACTIVE=prod" -p 8080:8080 -t onboarding

You can have any number of profiles applied at the same time.

Bringing apps together

It is recommended to use the Jmix application in a containerized environment with a server database. To simplify its installation, we can write a docker-compose file that will do everything for us.

Let’s create a docker/docker-compose.yml file:

name: onboarding
services:
onboarding-postgres:
container_name: onboarding-postgres
image: postgres:latest
ports:
- "5432:5432"
volumes:
- postgres:/var/lib/postgresql/data
- storage:/storage
environment:
- POSTGRES_USER=onboarding
- POSTGRES_PASSWORD=onboarding
- POSTGRES_DB=onboarding
onboarding:
container_name: onboarding
image: onboarding:latest
ports:
- "8080:8080"
environment:
- DB_VENDOR=postgresql
- DB_HOST=onboarding-postgres
- DB_USER=onboarding
- DB_PASSWORD=onboarding
- DB_PORT=5432
- DB_NAME=onboarding
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- onboarding-postgres
volumes:
postgres: {}
storage: {}

If you’re working on MacOS to run your build image locally, you should modify the docker-compose.yml file with an additional platform parameter in the onboarding service section:

name: onboarding
services:
onboarding-postgres:
container_name: onboarding-postgres
image: postgres:latest
ports:
- "5432:5432"
volumes:
- postgres:/var/lib/postgresql/data
- storage:/storage
environment:
- POSTGRES_USER=onboarding
- POSTGRES_PASSWORD=onboarding
- POSTGRES_DB=onboarding
onboarding:
platform: linux/amd64
container_name: onboarding
image: onboarding:latest
ports:
- "8080:8080"
environment:
- DB_VENDOR=postgresql
- DB_HOST=onboarding-postgres
- DB_USER=onboarding
- DB_PASSWORD=onboarding
- DB_PORT=5432
- DB_NAME=onboarding
- SPRING_PROFILES_ACTIVE=prod
depends_on:
- onboarding-postgres
volumes:
postgres: {}
storage: {}

Now, we can run everything with just one command:

docker compose -f docker/docker-compose.yml up -d --force-recreate

Where --force-recreate will tell Docker Compose to re-fetch an image from the registry. The -d option will detach running from the command line to the background.

You will not see any logs output right in the console, so to know if containers are successfully up and running you will have to use some more commands.

Get Jmix’s stories in your inbox

Join Medium for free to get updates from this writer.

You can check the status with ‘docker ps’ command:

docker ps

and read more info with ‘docker logs’ command:

docker logs onboarding

Image tooling alternatives

Alternatively, you can build your container using Google Jib tooling. Simply add the plugin to your project’s build.gradle file:

id 'com.google.cloud.tools.jib' version '3.2.0'

And run the building command:

./gradlew jib -Djib.to.image=onboarding -Djib.from.image=eclipse-temurin:21-jre

As you can see, with Jib, we can easily change the base image and many other build/runtime options just by entering command-line arguments.

With Jib, you can completely skip an image registry by using the jibBuildTar task, even if you need to deploy your containers to a remote server. You can upload the produced achieve file manually and load it with the ‘docker load’ command.

Jib can build images without a Docker daemon, which is helpful when Continuous Integration software runs inside containers and has no access to Docker daemon sockets.

In case you still want to use the Docker daemon, use jibDockerBuild instead of jib in the command above.

Another good thing is that Jib images are reproducible. That means the same builds will give the same image size and digest hash, which may reduce unnecessary executions and help your security.

To check what is included in your container image, you can use the dive tool as follows:

dive onboarding

If you want to just see layer statistics ‘docker history’ command can be enough.

In most cases, Jib builds look faster. According to articles on the internet (like this one), it is much faster than Buildpacks on clean builds and rebuilding with changes to be applied. Besides the speed, it has the most efficient image size. Only well-formed raw Dockerfiles can compete with it in efficiency.

Switching to raw Dockerfile

The simplest Dockerfile has just a few lines:

FROM openjdk:21-slim  
EXPOSE 8080
COPY build/libs/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

To build it, run the following command:

docker build . -t onboarding

But there are some things you should know about Dockerfile defaults:

1. The openjdk image is Deprecated:

Press enter or click to view image in full size
The openjdk image is Deprecated

2. The current image version is insanely vulnerable:

Press enter or click to view image in full size
The current image version is insanely vulnerable

3. This image stats: total size, build speed, etc., are worse than even when you use CNB defaults

Happily, you can easily change your base image to eclipse-temurin or any other alternative. With this change, you may gain the following benefits:

  1. Best OpenJDK solution suggested by themselves.
  2. Similar image size: 210.63 MB (OpenJDK) vs 153.43 MB (Eclipse Temurin).
  3. The image is based on a smaller Alpine Linux.
  4. Fewer Vulnerabilities (4x medium).

The good thing about having your own Dockerfile is that you can easily improve your builds, e.g., by adding layers.

FROM eclipse-temurin:21-jdk-alpine AS builder 
WORKDIR /application
COPY --chown=gradle:gradle . /application

RUN ./gradlew -Pvaadin.productionMode=true clean build -x test --no-daemon

FROM eclipse-temurin:21-jdk-alpine AS layers
ENV APP_NAME="Onboarding-0.0.1-SNAPSHOT.jar"
WORKDIR /application
COPY --from=builder /application/build/libs/${APP_NAME} /app/${APP_NAME}
RUN java -Djarmode=layertools -jar ${APP_NAME} extract

FROM eclipse-temurin:21-jre-alpine

WORKDIR /application
COPY --from=layers application /dependencies/ ./
COPY --from=layers application /spring-boot-loader/ ./
COPY --from=layers application /snapshot-dependencies/ ./
COPY --from=layers application /application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

This file can be visually divided into three parts. Each one starts with the FROM keyword that specifies its base image.

The first section is used to build application code only. It will run each time your sources have changed. The rest will be rebuilt only after you have changed dependency versions or in case you have cleaned all cached data.
Layer separation allows Docker to cache the application on every layer and rebuild and transfer images faster when you make changes in your application code, which is most often the case in application development.

With your own Dockerfile, you can customize your build by adding packages or any other resources, e.g. to install fonts for a better look’n’feel adding the following line:

RUN apt update && apt install -y libfreetype6 fonts-dejavu fontconfig

or for alpine-based images with:

RUN apk --update add fontconfig ttf-dejavu

It's better to have layers than not to have them, but a smaller number of layers can be more optimal for image size and speed. Consider merging related RUN command contents where possible with &&. In addition, reorder your Dockerfile data manipulation commands like ADD, COPY, and RUN so that less frequently changed data-related commands are placed before frequently changed ones.

One more good practice is to use a non-root user to run your application:

RUN adduser -S spring-user 
USER spring-user

You can generate an enhanced Dockerfile version similar to the one described above using the Jmix Studio plugin panel with just a few clicks.

Press enter or click to view image in full size
You can generate an enhanced Dockerfile version using the Jmix Studio plugin panel with just a few clicks.

Finally, you can adjust JVM runtime parameters using environment variables defined in the Dockerfile, but it’s more effective to utilize higher-level abstractions such as a Compose file or Helm(Kubernetes) configuration.

onboarding: 
container_name: onboarding
image: onboarding:latest
ports:
- "8080:8080"
environment:
- DB_VENDOR=postgresql
- DB_HOST=onboarding-postgres
- DB_USER=onboarding
- DB_PASSWORD=onboarding
- DB_PORT=5432
- DB_NAME=onboarding
- JAVA_TOOL_OPTIONS=-Xmx1548m -XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40
- BPL_JVM_THREAD_COUNT=100

For modern JDK versions, it is recommended to adjust memory limits using the -Xmx and -XX:MaxRam parameters, but without specifying -Xms, it is obsolete. Additionally, consider tweaking the -XX:MaxMetaspaceSize parameter, which limits the area of class name definitions as needed. When using build packs, memory usage settings are configured based on boot-time free memory detection. This approach is beneficial when your cluster restricts available memory for each container.

​Moreover, to not fall into unexpected errors ensure that memory settings reflect real-world usage statistics obtained through load testing, rather than setting minimal or under requirement values. It’s advisable to allocate extra resources to handle peak loads effectively. The less available RAM memory makes JVM to be worse in performance because GC runs more often. The outage of available memory can block application operations. Java is designed to be more performant by solving memory allocation consumption and thread-blocking problems. For regular applications, each memory allocation call is done with interruption instruction that allows the Operating System to pessimize application running priority in favor of other processes. Each time non-java application execution enters and leaves a processing block-like function or loop, it can run memory allocation operations as far as implicit garbage collection. To not be interrupted by memory management during processing, Java implements pre-allocated memory storage called Heap that allows it to work without the risk of being interrupted by memory allocation calls. Giving JVM less available memory for operation means fighting with its design and hardware architecture increasing the resource conflict. When you reduce memory usage, ensure you are not causing an increase in CPU load with consumption tweaks.

The final word

In this article, we observed that aside from the first-look complexity, the closer to raw Dockerfiles you are, the easier it is to tweak the image for your project’s special needs. At the same time, many features offered by image build tooling can be achieved by enhancing Dockerfiles and leveraging capabilities provided by clustering software.

Jmix makes this process even more streamlined by providing out-of-the-box support for Docker containers. With its integration into popular development environments like IntelliJ IDEA, Jmix simplifies the setup, build, and deployment of your applications, allowing you to focus on business logic rather than infrastructure concerns. Whether you’re working on a small-scale project or a large enterprise system, Jmix’s flexibility ensures your containerized apps run smoothly in any environment, while also providing easy integration with backend databases and services.

--

--

Jmix
Jmix

Written by Jmix

Jmix accelerates full-stack development for Java devs by combining Spring Boot and Vaadin into an intuitive, efficient framework for building scalable web apps