Containerizing microservices with Podman

duration 20 minutes


Learn how to containerize and run your microservices on Open Liberty using Podman.

What you’ll learn

You can easily deploy your microservices in different environments in a lightweight and portable manner by using containers. From development to production and across your DevOps environments, you can deploy your microservices consistently and efficiently with containers. You can run a container from a container image, which can be defined by a Containerfile file or a Dockerfile file. Each container image is a package of what you need to run your microservice or application, from the code to its dependencies and configuration.

You’ll learn how to build container images and run containers using the Pod Manager tool (Podman) for your microservices. You’ll construct Containerfile files, create container images by using the podman build command, and run the image as containers by using podman run command.

Podman and Buildah are related open-source container tools built to run on most Linux platforms and more. Buildah is designed specifically for building container images from either a Containerfile file or the command line. You can use Podman to maintain those images, and to create and run containers. Podman incorporates Buildah functions to create the container image that it uses.

The two microservices that you’ll be working with are called system and inventory. The system microservice returns the JVM system properties of the running container. The inventory microservice adds the properties from the system microservice to the inventory. This guide demonstrates how both microservices can run and communicate with each other in different containers.

Additional prerequisites

Before you begin, Podman needs to be installed. For installation instructions, refer to the official Podman documentation. You will build and run the microservices in containers.

If you are running Mac or Windows, make sure to start your Podman-managed VM before you proceed.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone
cd guide-containerize-podman

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Packaging your microservices

Navigate to the start directory to begin.

You can find the starting Java project in the start directory. This project is a multi-module Maven project that is made up of the system and inventory microservices. Each microservice is located in its own corresponding directory, system and inventory.

To try out the microservices by using Maven, run the following Maven goal to build the system microservice and run it inside Open Liberty:

mvn -pl system liberty:run

Open another command-line session and run the following Maven goal to build the inventory microservice and run it inside Open Liberty:

mvn -pl inventory liberty:run

After you see the following message in both command-line sessions, both of your services are ready:

The defaultServer server is ready to run a smarter planet.

To access the inventory service, which displays the current contents of the inventory, see http://localhost:9081/inventory/systems.

To access the system service, which shows the system properties of the running JVM, see http://localhost:9080/system/properties.

You can add the system properties of your localhost to the inventory service at http://localhost:9081/inventory/systems/localhost.

After you are finished checking out the microservices, stop the Open Liberty servers by pressing CTRL+C in the command-line sessions where you ran the servers. Alternatively, you can run the liberty:stop goal in another command-line session:

mvn -pl system liberty:stop
mvn -pl inventory liberty:stop

To package your microservices, run the Maven package goal to build the application .war files from the start directory so that the .war files are in the system/target and inventory/target directories.

mvn package

To learn more about RESTful web services and how to build them, see Creating a RESTful web service for details about how to build the system service. The inventory service is built in a similar way.

Building your container images

A container image is a binary file. It is made up of multiple layers and is used to run code in a container. Images are built from instructions in Containerfile files to create a containerized version of the application.

Containerfile and Dockerfile files use the same syntax. Podman can build your container image by using either Containerfile files or Dockerfile files. Containerfile files are used in this guide.

A Containerfile file is a collection of instructions for building a container image that can then be run as a container. These files can be interpreted by Buildah directly or through Podman. The podman build command uses Buildah to build your container image. As each instruction in a Containerfile file runs, a new image layer is created. These layers, which are known as intermediate images, are created when a change is made to your container image.

Learn more about Podman on the official Podman page.

Creating your Containerfile files

You will be creating two container images to run the inventory service and system service. The first step is to create Containerfile files for both services.

In this guide, you’re using an official image from the IBM Container Registry (ICR),, as your parent image. This image is tagged with the word full, meaning it includes all Liberty features. full images include all available features and are recommended for development only because they contain features that may not be required by your application and will significantly expand the image size.

To minimize your image footprint in production, you can use one of the kernel-slim images, such as This image installs the basic runtime. You can then add all the necessary features for your application with the usage pattern that is detailed in the Open Liberty container image documentation. To use the default image available for Open Liberty, define the FROM instruction as FROM You can find all official images on the Open Liberty container image repository.

Create the Containerfile file for the inventory service.


 1# tag::from[]
 3# end::from[]
 8# tag::label[]
10  org.opencontainers.image.authors="Your Name" \
11  org.opencontainers.image.vendor="Open Liberty" \
12  org.opencontainers.image.url="local" \
13  org.opencontainers.image.source="" \
14  org.opencontainers.image.version="$VERSION" \
15  org.opencontainers.image.revision="$REVISION" \
16  vendor="Open Liberty" \
17  name="inventory" \
18  version="$VERSION-$REVISION" \
19  summary="The inventory microservice from the Containerizing microservices guide" \
20  description="This image contains the inventory microservice running with the Open Liberty runtime."
21# end::label[]
23# tag::copy-config[]
24# tag::config-userID[]
25COPY --chown=1001:0 \
26# end::config-userID[]
27    # tag::inventory-config[]
28    src/main/liberty/config \
29    # end::inventory-config[]
30    # tag::config[]
31    /config/
32    # end::config[]
33# end::copy-config[]
35# tag::copy-war[]
36# tag::war-userID[]
37COPY --chown=1001:0 \
38# end::war-userID[]
39    # tag::inventory-war[]
40    target/inventory.war \
41    # end::inventory-war[]
42    # tag::config-apps[]
43    /config/apps
44    # end::config-apps[]
45# end::copy-war[]
47# tag::configure-sh[]
49# end::configure-sh[]

The FROM instruction initializes a new build stage, which indicates the parent image of the built image. If you don’t need a parent image, then you can use FROM scratch, which makes your image a base image.

Furthermore, you can label your container images with the LABEL command. The label information can help you manage your images.

The COPY instructions are structured as COPY [--chown=<user>:<group>] <source> <destination>. They copy local files into the specified destination within your container image. In this case, the inventory server configuration files that are located at src/main/liberty/config are copied to the /config/ destination directory. The inventory application WAR file inventory.war, which was created from running mvn package, is copied to the /config/apps destination directory.

The COPY instructions use the 1001 user ID and 0 group because all official Open Liberty base images, including used in this case, run by default with the USER 1001 (non-root) user for security purposes. Otherwise, the files and directories that are copied over are owned by the root user and a non-root user will be unable to access them.

Place the RUN command at the end to get a pre-warmed container image. It improves the startup time of running your container especially for production deployment.

The Containerfile file for the system service follows the same instructions as the inventory service, except that some labels are updated, and the system.war archive is copied into /config/apps.

Create the Containerfile file for the system service.


 7  org.opencontainers.image.authors="Your Name" \
 8  org.opencontainers.image.vendor="Open Liberty" \
 9  org.opencontainers.image.url="local" \
10  org.opencontainers.image.source="" \
11  org.opencontainers.image.version="$VERSION" \
12  org.opencontainers.image.revision="$REVISION" \
13  vendor="Open Liberty" \
14# tag::name[]
15  name="system" \
16# end::name[]
17  version="$VERSION-$REVISION" \
18# tag::summary[]
19  summary="The system microservice from the Containerizing microservices guide" \
20  description="This image contains the system microservice running with the Open Liberty runtime."
21# end::summary[]
23COPY --chown=1001:0 src/main/liberty/config /config/
25# tag::copy-war[]
26COPY --chown=1001:0 target/system.war /config/apps
27# end::copy-war[]

Building your container image

Now that your microservices are packaged and your Containerfile files are written, you will build your container images by using the podman build command.

Run the following command to download or update to the latest Open Liberty container image:

podman pull

Run the following commands to build container images for your application:

podman build -t system:1.0-SNAPSHOT system/.
podman build -t inventory:1.0-SNAPSHOT inventory/.

The -t flag in the podman build command tags the image in the name[:tag] format. The tag for an image describes the specific image version. If the optional [:tag] tag is not specified, the latest tag is created by default.

To verify that the images are built, run the podman images command to list all local container images:

podman images

Or, run the podman images command with --filter option to list your images:

podman images -f "label=org.opencontainers.image.authors=Your Name"

Your inventory and system images appear in the list of all container images:

REPOSITORY            TAG           IMAGE ID      CREATED        SIZE
localhost/inventory   1.0-SNAPSHOT  9d991299725c  4 minutes ago  933 MB
localhost/system      1.0-SNAPSHOT  a9b29bc94afd  5 minutes ago  931 MB

Running your microservices in containers

Now that your two images are built, you will run your microservices in containers:

podman run -d --name system -p 9080:9080 system:1.0-SNAPSHOT
podman run -d --name inventory -p 9081:9081 inventory:1.0-SNAPSHOT

The following table describes the flags in these commands:

Flag Description


Runs the container in the background.


Specifies a name for the container.


Maps the host ports to the container ports. For example: -p <HOST_PORT>:<CONTAINER_PORT>

Next, run the podman ps command to verify that your containers are started:

podman ps

Make sure that your containers are running and show Up as their status:

CONTAINER ID    IMAGE                             COMMAND                CREATED          STATUS          PORTS                                        NAMES
2b584282e0f5    localhost/inventory:1.0-SNAPSHOT  /opt/ol/wlp/bin/s...   2 seconds ago    Up 1 second     9080/tcp, 9443/tcp,>9081/tcp   inventory
99a98313705f    localhost/system:1.0-SNAPSHOT     /opt/ol/wlp/bin/s...   3 seconds ago    Up 2 seconds>9080/tcp, 9443/tcp             system

If a problem occurs and your containers exit prematurely, the containers don’t appear in the container list that the podman ps command displays. Instead, your containers appear with an Exited status when you run the podman ps -a command. Run the podman logs system and podman logs inventory commands to view the container logs for any potential problems. Run the podman stats system and podman stats inventory commands to display a live stream of usage statistics for your containers. You can also double-check that your Containerfile files are correct. When you find the cause of the issues, remove the faulty containers with the podman rm system and podman rm inventory commands. Rebuild your images, and start the containers again.

To access the application, go to the http://localhost:9081/inventory/systems URL. An empty list is expected because no system properties are stored in the inventory yet.

Next, retrieve the system container’s IP address by running the following:

podman inspect -f "{{.NetworkSettings.IPAddress }}" system

The command returns the system container IP address:

In this case, the IP address for the system service is Take note of this IP address to construct the URL to view the system properties.

Go to the http://localhost:9081/inventory/systems/[system-ip-address] URL by replacing [system-ip-address] with the IP address that you obtained earlier. You see a result in JSON format with the system properties of your local JVM. When you go to this URL, these system properties are automatically stored in the inventory. Go back to the http://localhost:9081/inventory/systems URL and you see a new entry for [system-ip-address].

Externalizing server configuration


 1<server description="Sample Liberty server">
 3  <featureManager>
 4    <feature>jaxrs-2.1</feature>
 5    <feature>jsonp-1.1</feature>
 6    <feature>cdi-2.0</feature>
 7    <feature>mpConfig-2.0</feature>
 8  </featureManager>
10  <!-- tag::httpPort[] -->
11  <variable name="default.http.port" defaultValue="9081" />
12  <!-- end::httpPort[] -->
13  <variable name="default.https.port" defaultValue="9444" />
15  <!-- tag::httpEndpoint[] -->
16  <httpEndpoint httpPort="${default.http.port}" httpsPort="${default.https.port}"
17      id="defaultHttpEndpoint" host="*" />
18  <!-- end::httpEndpoint[] -->
20  <webApplication location="inventory.war" contextRoot="/">
22  </webApplication>

As mentioned at the beginning of this guide, one of the advantages of using containers is that they are portable and can be moved and deployed efficiently across all of your DevOps environments. Configuration often changes across different environments, and by externalizing your server configuration, you can simplify the development process.

Imagine a scenario where you are developing an Open Liberty application on port 9081 but to deploy it to production, it must be available on port 9091. To manage this scenario, you can keep two different versions of the server.xml file; one for production and one for development. However, trying to maintain two different versions of a file might lead to mistakes. A better solution would be to externalize the configuration of the port number and use the value of an environment variable that is stored in each environment.

In this example, you will use an environment variable to externally configure the HTTP port number of the inventory service.

In the inventory/server.xml file, the default.http.port variable is declared and is used in the httpEndpoint element to define the service endpoint. The default value of the default.http.port variable is 9081. However, this value is only used if no other value is specified. You can replace this value in the container by using the -e flag for the podman run command.

Run the following commands to stop and remove the inventory container and rerun it with the default.http.port environment variable set:

podman stop inventory
podman rm inventory
podman run -d --name inventory -e default.http.port=9091 -p 9091:9091 inventory:1.0-SNAPSHOT

The -e flag can be used to create and set the values of environment variables in a container. In this case, you are setting the default.http.port environment variable to 9091 for the inventory container. The -p flag then maps the local port to the new container port that was specified via the environment variable.

Now, when the service is starting up, Open Liberty finds the default.http.port environment variable and uses it to set the value of the default.http.port variable to be used in the HTTP endpoint.

The inventory service is now available on the new port number that you specified. You can see the contents of the inventory at the http://localhost:9091/inventory/systems URL. You can add your local system properties at http://localhost:9091/inventory/systems/[system-ip-address] by replacing [system-ip-address] with the IP address that you obtained in the previous section. The system service remains unchanged and is available at the http://localhost:9080/system/properties URL.

You can externalize the configuration of more than just the port numbers. To learn more about Open Liberty server configuration, check out the Server Configuration Overview docs.

Testing the microservices

You can test your microservices manually by hitting the endpoints or with automated tests that check your running containers.

Create the SystemEndpointIT class.

 1// tag::copyright[]
 3 * Copyright (c) 2018, 2020 IBM Corporation and others.
 4 * All rights reserved. This program and the accompanying materials
 5 * are made available under the terms of the Eclipse Public License v1.0
 6 * which accompanies this distribution, and is available at
 7 *
 8 *
 9 * Contributors:
10 *     IBM Corporation - Initial implementation
11 *******************************************************************************/
12// end::copyright[]
15import static org.junit.jupiter.api.Assertions.assertEquals;
24import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
25import org.junit.jupiter.api.AfterEach;
26import org.junit.jupiter.api.BeforeAll;
27import org.junit.jupiter.api.BeforeEach;
28import org.junit.jupiter.api.Test;
30public class SystemEndpointIT {
32    private static String clusterUrl;
34    private Client client;
36    @BeforeAll
37    public static void oneTimeSetup() {
38        String nodePort = System.getProperty("system.http.port");
39        clusterUrl = "http://localhost:" + nodePort + "/system/properties/";
40    }
42    @BeforeEach
43    public void setup() {
44        client = ClientBuilder.newBuilder()
45                    .hostnameVerifier(new HostnameVerifier() {
46                        public boolean verify(String hostname, SSLSession session) {
47                            return true;
48                        }
49                    })
50                    .build();
51    }
53    @AfterEach
54    public void teardown() {
55        client.close();
56    }
58    // tag::testGetProperties[]
59    @Test
60    public void testGetProperties() {
61        Client client = ClientBuilder.newClient();
62        client.register(JsrJsonpProvider.class);
64        WebTarget target =;
65        Response response = target.request().get();
67        assertEquals(200, response.getStatus(), 
68            "Incorrect response code from " + clusterUrl);
69        response.close();
70    }
71    // end::testGetProperties[]

The testGetProperties() method checks for a 200 response code from the system service endpoint.

Create the InventoryEndpointIT class.

  1// tag::copyright[]
  3 * Copyright (c) 2018, 2020 IBM Corporation and others.
  4 * All rights reserved. This program and the accompanying materials
  5 * are made available under the terms of the Eclipse Public License v1.0
  6 * which accompanies this distribution, and is available at
  7 *
  8 *
  9 * Contributors:
 10 *     IBM Corporation - Initial implementation
 11 *******************************************************************************/
 12// end::copyright[]
 15import static org.junit.jupiter.api.Assertions.assertEquals;
 16import static org.junit.jupiter.api.Assertions.assertTrue;
 18import javax.json.JsonObject;
 26import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
 27import org.junit.jupiter.api.AfterAll;
 28import org.junit.jupiter.api.BeforeAll;
 29import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
 30import org.junit.jupiter.api.Order;
 31import org.junit.jupiter.api.Test;
 32import org.junit.jupiter.api.TestMethodOrder;
 35public class InventoryEndpointIT {
 37    private static String invUrl;
 38    private static String sysUrl;
 39    private static String systemServiceIp;
 41    private static Client client;
 43    @BeforeAll
 44    public static void oneTimeSetup() {
 46        String invServPort = System.getProperty("inventory.http.port");
 47        String sysServPort = System.getProperty("system.http.port");
 49        // tag::systemServiceIp[]
 50        systemServiceIp = System.getProperty("system.ip");
 51        // end::systemServiceIp[]
 53        invUrl = "http://localhost" + ":" + invServPort + "/inventory/systems/";
 54        sysUrl = "http://localhost" + ":" + sysServPort + "/system/properties/";
 56        client = ClientBuilder.newBuilder().hostnameVerifier(new HostnameVerifier() {
 57            public boolean verify(String hostname, SSLSession session) {
 58                return true;
 59            }
 60        }).build();
 62        client.register(JsrJsonpProvider.class);
 63 + "reset").request().post(null);
 64    }
 66    @AfterAll
 67    public static void teardown() {
 68        client.close();
 69    }
 71    // tag::tests[]
 72    // tag::testEmptyInventory[]
 73    @Test
 74    @Order(1)
 75    public void testEmptyInventory() {
 76        Response response = this.getResponse(invUrl);
 77        this.assertResponse(invUrl, response);
 79        JsonObject obj = response.readEntity(JsonObject.class);
 81        int expected = 0;
 82        int actual = obj.getInt("total");
 83        assertEquals(expected, actual,
 84                        "The inventory should be empty on application start but it wasn't");
 86        response.close();
 87    }
 88    // end::testEmptyInventory[]
 90    // tag::testHostRegistration[]
 91    @Test
 92    @Order(2)
 93    public void testHostRegistration() {
 94        this.visitSystemService();
 96        Response response = this.getResponse(invUrl);
 97        this.assertResponse(invUrl, response);
 99        JsonObject obj = response.readEntity(JsonObject.class);
101        int expected = 1;
102        int actual = obj.getInt("total");
103        assertEquals(expected, actual,
104                        "The inventory should have one entry for " + systemServiceIp);
106        boolean serviceExists = obj.getJsonArray("systems").getJsonObject(0)
107                        .get("hostname").toString().contains(systemServiceIp);
108        assertTrue(serviceExists,
109                        "A host was registered, but it was not " + systemServiceIp);
111        response.close();
112    }
113    // end::testHostRegistration[]
115    // tag::testSystemPropertiesMatch[]
116    @Test
117    @Order(3)
118    public void testSystemPropertiesMatch() {
119        Response invResponse = this.getResponse(invUrl);
120        Response sysResponse = this.getResponse(sysUrl);
122        this.assertResponse(invUrl, invResponse);
123        this.assertResponse(sysUrl, sysResponse);
125        JsonObject jsonFromInventory = (JsonObject) invResponse
126                        .readEntity(JsonObject.class).getJsonArray("systems")
127                        .getJsonObject(0).get("properties");
129        JsonObject jsonFromSystem = sysResponse.readEntity(JsonObject.class);
131        String osNameFromInventory = jsonFromInventory.getString("");
132        String osNameFromSystem = jsonFromSystem.getString("");
133        this.assertProperty("", systemServiceIp, osNameFromSystem,
134                        osNameFromInventory);
136        String userNameFromInventory = jsonFromInventory.getString("");
137        String userNameFromSystem = jsonFromSystem.getString("");
138        this.assertProperty("", systemServiceIp, userNameFromSystem,
139                        userNameFromInventory);
141        invResponse.close();
142        sysResponse.close();
143    }
144    // end::testSystemPropertiesMatch[]
146    // tag::testUnknownHost[]
147    @Test
148    @Order(4)
149    public void testUnknownHost() {
150        Response response = this.getResponse(invUrl);
151        this.assertResponse(invUrl, response);
153        Response badResponse = + "badhostname")
154                        .request(MediaType.APPLICATION_JSON).get();
156        String obj = badResponse.readEntity(String.class);
158        boolean isError = obj.contains("error");
159        assertTrue(isError,
160                        "badhostname is not a valid host but it didn't raise an error");
162        response.close();
163        badResponse.close();
164    }
165    // end::testUnknownHost[]
166    // end::tests[]
168    // Returns response information from the specified URL.
169    private Response getResponse(String url) {
170        return;
171    }
174    // Asserts that the given URL has the correct response code of 200.
175    private void assertResponse(String url, Response response) {
176        assertEquals(200, response.getStatus(), "Incorrect response code from " + url);
177    }
179    // Asserts that the specified JVM system property is equivalent in both the
180    // system and inventory services.
181    private void assertProperty(String propertyName, String hostname, String expected,
182                    String actual) {
183        assertEquals(expected, actual, "JVM system property [" + propertyName + "] "
184                        + "in the system service does not match the one stored in "
185                        + "the inventory service for " + hostname);
186    }
188    // Makes a simple GET request to inventory/localhost.
189    private void visitSystemService() {
190        Response response = this.getResponse(sysUrl);
191        this.assertResponse(sysUrl, response);
192        response.close();
194        Response targetResponse = + systemServiceIp).request()
195                        .get();
197        targetResponse.close();
198    }
  • The testEmptyInventory() method checks that the inventory service has a total of 0 systems before anything is added to it.

  • The testHostRegistration() method checks that the system service was added to inventory properly.

  • The testSystemPropertiesMatch() checks that the system properties match what was added into the inventory service.

  • The testUnknownHost() method checks that an error is raised if an unknown host name is being added into the inventory service.

  • The systemServiceIp variable has the same value as the IP address that you retrieved in the previous section when you manually added the system service into the inventory service. This value of the IP address is passed in when you run the tests.

Running the tests

Run the Maven package goal to compile the test classes. Run the Maven failsafe goal to test the services that are running in the containers by replacing the [system-ip-address] with the IP address that you determined previously.

mvn package
mvn failsafe:integration-test -Dsystem.ip=[system-ip-address] -Dinventory.http.port=9091 -Dsystem.http.port=9080

If the tests pass, you see output similar to the following example:

 T E S T S
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.653 s - in


Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

 T E S T S
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.935 s - in


Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

When you are finished with the services, run the following commands to stop and remove your containers:

podman stop inventory system
podman rm inventory system

Great work! You’re done!

You have just built container images and run two microservices on Open Liberty in containers using Podman.

Guide Attribution

Containerizing microservices with Podman by Open Liberty is licensed under CC BY-ND 4.0

Copied to clipboard
Copy code block
Copy file contents


Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.