This is a complete, beginner-friendly, end-to-end guide for building, containerizing, and testing a Java 21 REST API with Open Liberty and Podman, with clear descriptions and step-by-step IntelliJ IDEA instructions.
1. Prerequisites and Setup
Purpose:
Prepare your development environment with all necessary tools to build, containerize, and run your Java 21 REST API with Open Liberty and Podman.
A. Install Java 21
- Download and install Java 21 JDK (e.g., from Adoptium).
- Set
JAVA_HOME
to your Java 21 installation. - On Windows, add
JAVA_HOME/bin
to yourPATH
.
B. Install Maven
- Download and install Maven 3.8+ (official site).
- Verify installation:
1 |
mvn -v |
And you should see something similar to the below:

C. Install Podman or Podman Desktop
- Download Podman Desktop for Windows/macOS or install Podman CLI for Linux.
- On Windows/macOS, initialize the Podman machine:
1 |
podman machine init<br>podman machine start |
Verify Podman works:
1 |
podman info<br>podman run --rm hello-world |
D. Install IntelliJ IDEA
- Download and install IntelliJ IDEA Community or Ultimate.
2. Project Structure
Purpose:
Organize your project into a clear multi-module Maven layout, separating the REST application and integration tests for modular builds and containerization.

3. Creating the Project in IntelliJ IDEA
Purpose:
Set up your Maven multi-module project using IntelliJ IDEA’s tools for easy project management.
A. Create Parent Project
- Open IntelliJ IDEA.
- Click New Project.
- Select Maven as the project type.
- Set the GroupId (e.g.,
com.example
) and ArtifactId (e.g.,parent
). - Choose Java 21 as the JDK (see below if not listed).
- Click Create.
B. Add Java 21 JDK to IntelliJ
- Go to File → Project Structure → Project.
- Under Project SDK, click Add JDK and select your Java 21 install folder.
- Set Project language level to 21 (Preview) if you want Java 21 features.
C. Add Modules
- Right-click the parent project → New → Module → Maven.
- Name the first module
restful-war
(packaging:war
). - Repeat for
integration-tests
(packaging:jar
).
4. Parent POM: Java 21 Compilation
Purpose:
Ensure all modules compile to Java 21 bytecode, preventing runtime compatibility issues.
root/pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.14.0</version> <configuration> <release>21</release> </configuration> </plugin> </plugins> </build> |
Tip:
This ensures Java 21 compatibility even if your system JDK is newer.
5. RESTful Application Code
Purpose:
Define your REST application, path and endpoint.
restful-war/src/main/java/com/example/RestApplication.java
1 2 3 4 5 6 7 8 |
package com.example; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("/api") public class RestApplication extends Application { } |
restful-war/src/main/java/com/example/HelloResource.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.example; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/hello") public class HelloResource { @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { return "Hello, Open Liberty!"; } } |
6. Open Liberty Configuration
Purpose:
The server.xml configures the Open Liberty application server, in this case for the Jakarta EE features, HTTP ports, context root, and logging.
restful-war/src/main/liberty/config/server.xml
1 2 3 4 5 6 7 8 9 10 |
<server description="REST API Liberty server"> <featureManager> <feature>jakartaee-10.0</feature> <feature>restfulWS</feature> <feature>jsonb</feature> </featureManager> <httpEndpoint httpPort="9080" httpsPort="9443" id="defaultHttpEndpoint" host="*" /> <webApplication location="restful-war.war" contextRoot="/restful"/> <logging consoleLogLevel="INFO"/> </server> |
Tips:
- The API will be at
/restful/api/hello
. - Logs will be visible via
podman logs
.
7. WAR Module POM
Purpose:
The war module will build your web application resource (WAR) file. This POM declares the dependencies and packaging for your REST API WAR module.
restful-war/pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
<project> <parent> <groupId>com.example</groupId> <artifactId>parent</artifactId> <version>1.0-SNAPSHOT</version> </parent> <artifactId>restful-war</artifactId> <packaging>war</packaging> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> </dependencies> </project> |
8. Integration Tests Module: Copy WAR and Config
Purpose:
The integration test module is responsible for integration testing your endpoint. This will copy the compiled WAR and Liberty server configuration o the integration test module, run the config to setup your Podman container image and deploy your war to it, and then run an integration test against the application running in the container to confirm it works as expected.
The section of the pom below uses the maven-dependency-plugin to copy the war file to this module, and then the maven-resources-plugin to copy the open liberty configuration, (in this case just the server.xml) to this module.
integration-tests/pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>3.6.0</version> <executions> <execution> <id>copy-war</id> <phase>process-resources</phase> <goals><goal>copy</goal></goals> <configuration> <artifactItems> <artifactItem> <groupId>com.example</groupId> <artifactId>restful-war</artifactId> <version>${project.version}</version> <type>war</type> <outputDirectory>${project.build.directory}</outputDirectory> <destFileName>restful-war.war</destFileName> </artifactItem> </artifactItems> </configuration> </execution> </executions> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.3.1</version> <executions> <execution> <id>copy-liberty-config</id> <phase>process-resources</phase> <goals><goal>copy-resources</goal></goals> <configuration> <outputDirectory>${project.build.directory}/liberty-config</outputDirectory> <resources> <resource> <directory>${project.parent.basedir}/restful-war/src/main/liberty/config</directory> <filtering>false</filtering> </resource> </resources> </configuration> </execution> </executions> </plugin> |
9. Containerfile for Liberty Java 21
Purpose:
Rather than having a local install of open liberty, deploying the war and then running it, we are going to define the podman container image build instructions for Open Liberty running Java 21 and your application. We will use maven to run the build of the image
integration-tests/Containerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# install latest open liberty, and java 21 FROM icr.io/appcafe/open-liberty:kernel-slim-java21-openj9-ubi-minimal # Copy liberty config COPY target/liberty-config/ /config/ # Copy war file COPY target/*.war /config/apps/ # Download and setup open liberty features as defined in server.xml RUN features.sh # Make the application server port accessible from outside the container EXPOSE 9080 |
10. Podman Maven Plugin for Image Build
Purpose:
Automate Podman container image building within Maven using the Podman Maven plugin.
integration-tests/pom.xml (snippet)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<plugin> <groupId>nl.lexemmens</groupId> <artifactId>podman-maven-plugin</artifactId> <version>1.19.0</version> <executions> <execution> <id>build-image</id> <phase>pre-integration-test</phase> <goals> <goal>build</goal> </goals> </execution> </executions> <configuration> <imageName>integration-tests</imageName> <tags> <tag>latest</tag> <tag>${project.version}</tag> </tags> </configuration> </plugin> |
Tip:
Use a local image name like integration-tests
to avoid Podman registry errors.

11. Exec Maven Plugin: Container Lifecycle
Purpose:
Control container lifecycle during integration tests by stopping/removing old containers and running new ones, all triggered automatically in the Maven build phases.
integration-tests/pom.xml (snippet)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
<!-- Exec Maven Plugin: run and stop container for integration tests --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <id>stop-liberty-container</id> <phase>package</phase> <goals><goal>exec</goal></goals> <configuration> <executable>podman</executable> <arguments> <argument>stop</argument> <argument>--ignore</argument> <argument>${project.parent.artifactId}</argument> </arguments> </configuration> </execution> <execution> <id>remove-liberty-container</id> <phase>package</phase> <goals><goal>exec</goal></goals> <configuration> <executable>podman</executable> <arguments> <argument>rm</argument> <argument>--ignore</argument> <argument>${project.parent.artifactId}</argument> </arguments> </configuration> </execution> <execution> <id>prune-podman-images</id> <phase>package</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>podman</executable> <arguments> <argument>image</argument> <argument>prune</argument> <argument>-a</argument> <argument>-f</argument> </arguments> </configuration> </execution> <execution> <id>run-podman-container</id> <phase>pre-integration-test</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>podman</executable> <useMavenLogger>true</useMavenLogger> <arguments> <argument>run</argument> <argument>-d</argument> <argument>-p</argument> <argument>9080:9080</argument> <argument>--name</argument> <argument>${project.parent.artifactId}</argument> <argument>${project.parent.artifactId}</argument> </arguments> </configuration> </execution> </executions> </plugin> |
Tip:
Use multiple <execution>
blocks in a single plugin declaration for correct ordering.
12. JUnit Integration Test
Purpose:
This will verify your REST API endpoint is accessible and return the expected response by running the HTTP test against the containerized application.
integration-tests/src/test/java/com/example/HelloIT.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.example; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; import java.net.http.*; import java.net.URI; public class HelloIT { @Test void testHelloEndpoint() throws Exception { HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("http://localhost:9080/restful-p/api/hello")) .build(); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); assertEquals(200, response.statusCode()); assertEquals("Hello, Open Liberty!", response.body()); } } |
Tip:
Update the URL to match your context root (server.xml) , @ApplicationPath
(RestApplication.java) and @Path (HelloResource.java).
Add the maven failsafe plugin to ensure your integrations tests are run as part of the mvn clean install build of the parent.
13. Running and Testing in IntelliJ IDEA
Purpose:
Build, run, and test your project easily from IntelliJ IDEA.
A. Build the Project
- Right-click the parent project → Maven → Reload Project.
- Open a terminal (bottom panel) and run:
1 |
mvn clean install |
- Or use the Maven panel to run Lifecycle → clean and install.
This is what you would expect to see if all was successful

B. Run Integration Tests
- Right-click the
integration-tests
module → Run ‘All Tests’. - Or run:
1 |
mvn verify -pl integration-tests |
- Check the Run or Test Results window for test output.
C. Debugging and Logs
- Use the Terminal in IntelliJ to run Podman commands (see below).
- View container logs:
1 |
podman logs liberty-test |
D. HTTP Client in IntelliJ
- You can use IntelliJ’s built-in HTTP client (create a
.http
file) to manually test endpoints:
1 |
GET http://localhost:9080/restful-p/api/hello |
14. Useful Podman Commands
Purpose:
Provide handy Podman commands for manual container and image management during development and troubleshooting.
Step | Command/Action |
---|---|
Build all modules | mvn clean install |
Build integration image | mvn podman:build -pl integration-tests |
Run integration tests | mvn verify -pl integration-tests |
Manually run container | podman run --name liberty-test -p 9080:9080 integration-tests:latest |
Stop container | podman stop liberty-test |
Remove container | podman rm liberty-test |
View logs | podman logs liberty-test |
Exec into container | podman exec -it liberty-test sh |
15. Podman Image Management
Purpose:
Keep your Podman environment clean by pruning unused images and checking image build timestamps to avoid stale deployments.
- Prune unused images (careful: this deletes all unreferenced images!):
1 |
podman image prune -a -f |
Check image age:
1 |
podman history --human integration-tests:latest |
Check running containers:
1 |
podman ps -a |
16. Troubleshooting & Tips
Purpose:
Summarize common issues and best practices.
- 404 errors:
Double-check your context root,@ApplicationPath
, and test URLs. - Class version errors:
Ensure Maven is compiling with<release>21</release>
and your image uses Java 21. - Image not updating:
Usepodman build --no-cache ...
and confirm withpodman history
. - Podman registry errors:
Use local image names (notorg.example/...
) to avoid Podman trying to pull from a non-existent registry. - Order of plugin execution:
List plugins in<plugins>
in the order you want them to run for the same phase. - Multiple executions:
Use multiple<execution>
blocks inside a single plugin declaration, not multiple plugin declarations.
17. References
Purpose:
Official documentation and guides for further reading.