Podman for Java Developers: The Missing Tutorial


I have been using Docker since late 2013 and for me and many others Docker has revolutionised the way we build, package, and deploy software. As a community we are grateful to Docker and its creators. Docker is one of the first tools that I install on my dev machine. It used to be always running on my MacBook and anytime I wanted to try a new technology I preferred to install it using Docker. Just do a docker run <tech> and you are good to go. But, this has changed in the last couple of years. Docker for Mac is still installed but I no longer keep it running. The main reason for that has been the amount of resources it consumes, distracting fan noise, and MacBook becoming too hot. There are many issues filed in the Docker for Mac issue tracker on Github where developers have shared similar experience. Still, I kept using it as there was no good alternative available.

A couple of weeks back I learnt that Docker has changed its monetization strategy. Docker Desktop (Docker for Mac and Docker for Windows) will soon require subscription. From the Docker blog published on 31st August 2021 I quote:

  • Docker Desktop remains free for small businesses (fewer than 250 employees AND less than $10 million in annual revenue), personal use, education, and non-commercial open source projects.
  • It requires a paid subscription (Pro, Team or Business), starting at $5 per user per month, for professional use in larger businesses. You may directly purchase here, or share this post and our solution brief with your manager.
  • While the effective date of these terms is August 31, 2021, there is a grace period until January 31, 2022 for those that require a paid subscription to use Docker Desktop.

For organizations which are not small as per Docker’s definition of small will be required to pay a monthly per user subscription. I am not saying whether it is a good or bad strategy on behalf of Docker to monetize Docker Desktop. I personally don’t want to pay for Docker Desktop as I don’t care much about a fancy GUI. Most of my work is done using Docker CLI. This prompted me to look for other alternatives that can replace Docker Desktop.

In my research I found Podman, an open source project by Red Hat a worthy candidate to replace Docker Desktop. I wanted to take Podman for a test run to see if it is ready to replace Docker for Mac on my machine. My short answer is that it still needs more work. It is not a 1-1 replacement. You will have to make small changes.

In this post, I will show how we can use Podman with a real world application. Conduit is a real world blogging platform similar to Medium.com. Conduit was created by developers part of the realworld community.

You can clone the repository on your local machine by running the following Git command on your terminal.

git clone git@github.com:shekhargulati/conduit-podman-demo.git

Please change directory to conduit-podman-demo

Installing Podman on Mac

We will use Homebrew package manager to install Podman on our machine. Please run the following command.

brew install podman

The above install Podman CLI. You still need to run a Podman managed virtual machine on your host.

To do that run following commands:

podman machine init
podman machine start

You can list Podman managed VMs by running the following command.

podman machine list
NAME                     VM TYPE     CREATED     LAST UP
podman-machine-default*  qemu        5 days ago  Currently running

You can view installation information using podman info command.

Pull and Run Docker Images

Before we move on to conduit application let’s run the basic pull and run commands to get confidence that podman works for the most basic use case.

You can list images using images sub-command as shown below.

podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

Since this is a clean installation no image is found.

There is a one-to-one mapping between Docker and Podman commands. Just use podman instead of docker.

Let’s pull the Nginx image using the command shown below.

podman pull nginx

You will be greeted by the following error message. Not a great start. Something as simple as pulling an image failed.

Error: failed to parse "X-Registry-Auth" header for /v3.3.1/libpod/images/pull?alltags=false&arch=&authfile=&os=&password=&policy=always&quiet=false&reference=nginx&username=&variant=: error storing credentials in temporary auth file (server: "https://index.docker.io/v1/", user: ""): key https://index.docker.io/v1/ contains http[s]:// prefix

The reason for this is that Docker for Mac has created a file called ~/.docker/config.json that has an empty entry for https://index.docker.io/v1/ registry as shown below. Podman expects credentials since they are not present it fails.

{
    "auths": {
        "https://index.docker.io/v1/": {}
    },
    "credsStore": "desktop",
    "experimental": "disabled",
    "stackOrchestrator": "swarm",
    "currentContext": "default"
}

The fix is simple. You have to remove https://index.docker.io/v1/ entry as shown below.

{
    "auths": {
    },
    "credsStore": "desktop",
    "experimental": "disabled",
    "stackOrchestrator": "swarm",
    "currentContext": "default"
}

Let’s try to pull the image again. This time we get another error as shown below. Podman is not doing great. Getting started experience needs improvement especially people coming from Docker world.

Error: short-name resolution enforced but cannot prompt without a TTY

It turns out we have to specify the complete docker image URL as shown below.

podman pull docker.io/library/nginx:latest

The above will pull the image and you should be able to see that as shown below.

podman images
REPOSITORY               TAG         IMAGE ID      CREATED       SIZE
docker.io/library/nginx  latest      ad4c705f24d3  15 hours ago  138 MB

Let’s now try to run the Nginx container from the image we just pulled.

To run the container we will use our usual run sub-command as shown below. As you can see again it is 1-1 mapped to the docker run command.

podman run --name nginx -d -p 8000:80 nginx:latest

Then, you can list containers

podman ps
CONTAINER ID  IMAGE                           COMMAND               CREATED        STATUS            PORTS                 NAMES
1ff585e884b4  docker.io/library/nginx:latest  nginx -g daemon o...  3 seconds ago  Up 3 seconds ago  0.0.0.0:8000->80/tcp  nginx

So, we see our running container. Yay!

Now, I was expecting I should be able to access curl http://localhost:8000 but I got following error

curl: (7) Failed to connect to localhost port 8000: Connection refused

In the current Podman version(3.3.1) port forwarding from host to VM doesn’t work by just specifying -p option. You will have to use bridge network. This issue is now fixed and will be available in 3.3.2.

Stop and remove the container

podman stop nginx && podman rm nginx

Again, run the container but this use --network bridge as well.

podman run -d --name nginx --network bridge -p 8000:80 nginx:latest

Now, if we use curl we see default nginx page.

curl http://localhost:8000
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

Now, we know that we can get pull and run working. Let’s move ahead.

Setting docker alias

Since podman has one-to-many mapping with Docker CLI commands it is suggested that you set docker alias as shown below. This will also ensure that we don’t have to change our development workflow. The expectation is that it will make the switch seamless.

alias docker=podman

We will see later in the post that it is not sufficient. The better approach is to use symlink. I will cover that later. Let me not jump ahead.

Running conduit application in Podman containers

If you have not cloned the demo application yet please do that now so that you can try it on your machine as well.

git clone git@github.com:shekhargulati/conduit-podman-demo.git

Change directory to conduit-podman-demo.

conduit is like most modern web applications. It has a frontend component and a backend component. Frontend is a Vue application that we run in Nginx and Backend API server is a Spring Boot app that uses MySQL database.

-rw-r--r--   1 shekhargulati  staff   216B Sep 13 15:18 README.md
drwxr-xr-x  13 shekhargulati  staff   416B Sep 13 18:49 conduit-api
drwxr-xr-x  24 shekhargulati  staff   768B Sep 13 15:18 conduit-frontend

We will first build the frontend image and then we will build the backend. Change directory to conduit-frontend.

In the root of conduit-frontend you will find the Docker file as shown below. It is using Docker multi-build feature. We first build the frontend in a nodejs image and then package the frontend distribution in nginx image.

FROM node:10.20 as builder
WORKDIR /home/ui
COPY . .
RUN npm install
ARG VUE_APP_API_URL
ENV VUE_APP_API_URL $VUE_APP_API_URL
RUN npm run build

FROM nginx
COPY  --from=builder /home/ui/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Let’s build the image using docker build command as shown below.

docker build --build-arg VUE_APP_API_URL=http://localhost:8080/api -t com.conduitapp/frontend .

We will get following error.

[1/2] STEP 1/7: FROM node:10.20 AS builder
[2/2] STEP 1/4: FROM nginx
Error: error creating build container: short-name resolution enforced but cannot prompt without a TTY

We already know the answer we have to use the full image name i.e. instead of node:10.20 we have to use docker.io/library/node:10.20 and same for nginx as shown below.

FROM docker.io/library/node:10.20 as builder
WORKDIR /home/ui
COPY . .
RUN npm install
ARG VUE_APP_API_URL
ENV VUE_APP_API_URL $VUE_APP_API_URL
RUN npm run build

FROM docker.io/library/nginx
COPY  --from=builder /home/ui/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Run the docker build command again. This time image will get built successfully.

docker build --build-arg VUE_APP_API_URL=http://localhost:8080/api -t com.conduitapp/frontend .

We can check that our image is built.

podman images
REPOSITORY               TAG         IMAGE ID      CREATED        SIZE
om.conduitapp/frontend     latest      adf828f74654  20 hours ago   139 MB
docker.io/library/nginx  latest      ad4c705f24d3  3 days ago     138 MB
docker.io/library/node   10.20       c5f1efe092a0  16 months ago  941 MB

We can run our frontend as shown below. It will fail to connect with API but still app will load.

docker run -p 8000:80 --network bridge com.conduitapp/frontend

In the browser console you will see API connection errors as well.

Let’s build backend image now. For this we will not use docker build command directly. This project is configured to use palantir/gradle-docker plugin. In the build.gradle you will notice following.

docker {
    name "com.conduitapp/api"
    dockerfile file('src/main/resources/docker/Dockerfile')
    files bootJar.archivePath
    buildArgs(['JAR_FILE': "${bootJar.archiveName}"])
    pull false
}

The Dockerfile is shown below. We have changed base image to full name.

FROM docker.io/library/openjdk:8-jre-slim
ENV SPRING_DATASOURCE_URL jdbc:mysql://mysql:3306/conduit
ENV SPRING_DATASOURCE_USERNAME root
ENV SPRING_DATASOURCE_PASSWORD password
ENV APP_DIR /app
ARG JAR_FILE
ADD ${JAR_FILE} $APP_DIR/app.jar
WORKDIR $APP_DIR
EXPOSE 8080
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar","app.jar", "spring.datasource.url=${SPRING_DATASOURCE_URL}", "spring.datasource.username=${SPRING_DATASOURCE_USERNAME}","spring.datasource.password=${SPRING_DATASOURCE_PASSWORD}"]

Having successfully built the frontend image I was expecting this to be straightforward. The palantir/gradle-docker internally using docker CLI. Since I already have a docker alias set I was confident it will be a cakewalk. It turned out I spent the next 4 hours making it work.

The Gradle build kept failing. Make sure you are inside conduit-api directory.

./gradlew build docker

You must be wondering why. The reason was that I had Docker for Mac installed on my machine. I had stopped Docker for Mac but docker CLI was still in the path. When Gradle plugin looks for docker CLI it finds the actual Docker CLI not the alias. As I learnt, aliases are not expanded when calling them via a program. So, rather than using my docker alias which points to the podman Gradle plugin was using actual docker CLI. This means it was calling the wrong Docker REST API endpoint. Rather than calling API under /v3.3.1/libpod/build it was calling API under 1.4.0/build (Docker REST API).

I figured this out by looking at Podman logs. First ssh into the Podman machine using podman machine ssh and then run the following command.

podman --log-level=debug system service -t3600

I completely uninstalled Docker for Mac from my machine. After that I started seeing errors where docker CLI was not available since alias expansion was not happening. Next, I set up symlink and things started working.

ln -s /usr/local/bin/podman /usr/local/bin/docker

After making this change, I was able to successfully build the image using ./gradlew build docker.

docker images |grep conduit
com.conduitapp/frontend     latest      adf828f74654  21 hours ago   139 MB
com.conduitapp/api          latest      2c79c1c574f7  2 days ago     235 MB

If we run this image it will fail as we have not yet started MySQL container.

docker run -p 8080:8080 com.conduitapp/api
Caused by: java.net.UnknownHostException: mysql: Name or service not known
    at java.net.Inet6AddressImpl.lookupAllHostAddr(Native Method) ~[na:1.8.0_302]
    at java.net.InetAddress$2.lookupAllHostAddr(InetAddress.java:929) ~[na:1.8.0_302]
    at java.net.InetAddress.getAddressesFromNameService(InetAddress.java:1324) ~[na:1.8.0_302]
    at java.net.InetAddress.getAllByName0(InetAddress.java:1277) ~[na:1.8.0_302]
    at java.net.InetAddress.getAllByName(InetAddress.java:1193) ~[na:1.8.0_302]
    at java.ne.InetAddress.getAllByName(InetAddress.java:1127) ~[na:1.8.0_302]
    at com.mysql.jdbc.StandardSocketFactory.connect(StandardSocketFactory.java:188) ~[mysql-connector-java-5.1.47.jar!/:5.1.47]
    at com.mysql.jdbc.MysqlIO.<init>(MysqlIO.java:301) ~[mysql-connector-java-5.1.47.jar!/:5.1.47]
    ... 53 common frames omitted

Let’s run a MySQL container and connect it with the app.

It took some time to figure this out. Podman does not have a concept of --link nor two containers connected if they are part of the same network. You have to create a pod object and then create containers inside that pod.

podman pod create -p 8080:8080 --network bridge --name conduit

Now, we will create MySQL and API containers.

docker run -d --name mysql --pod conduit -e MYSQL_ROOT_PASSWORD=password -e MYSQL_DATABASE=conduit docker.io/library/mysql:5.6
docker run --pod conduit com.conduitapp/api

The logs confirm that API server is successfully able to connect to MySQL.

2021-09-16 06:04:22.674  INFO 1 --- [           main] .m.m.a.ExceptionHandlerExceptionResolver : Detected @ExceptionHandler methods in customizeExceptionHandler
2021-09-16 06:04:22.832  INFO 1 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2021-09-16 06:04:24.188  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2021-09-16 06:04:24.199  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Bean with name 'dataSource' has been autodetected for JMX exposure
2021-09-16 06:04:24.230  INFO 1 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource]
2021-09-16 06:04:24.421  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2021-09-16 06:04:24.449  INFO 1 --- [           main] io.spring.RealworldApplication           : Started RealworldApplication in 23.531 seconds (JVM running for 25.509)

If you hit curl command

curl --silent http://localhost:8080
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Conduit App</title>
</head>
<body>
<pre>
dddddddd
CCCCCCCCCCCCC                                               d::::::d                    iiii          tttt
CCC::::::::::::C                                               d::::::d                   i::::i      ttt:::t
CC:::::::::::::::C                                               d::::::d                    iiii       t:::::t
C:::::CCCCCCCC::::C                                               d:::::d                                t:::::t
C:::::C       CCCCCC   ooooooooooo   nnnn  nnnnnnnn        ddddddddd:::::d uuuuuu    uuuuuu  iiiiiiittttttt:::::ttttttt
C:::::C               oo:::::::::::oo n:::nn::::::::nn    dd::::::::::::::d u::::u    u::::u  i:::::it:::::::::::::::::t
C:::::C              o:::::::::::::::on::::::::::::::nn  d::::::::::::::::d u::::u    u::::u   i::::it:::::::::::::::::t
C:::::C              o:::::ooooo:::::onn:::::::::::::::nd:::::::ddddd:::::d u::::u    u::::u   i::::itttttt:::::::tttttt
C:::::C              o::::o     o::::o  n:::::nnnn:::::nd::::::d    d:::::d u::::u    u::::u   i::::i      t:::::t
C:::::C              o::::o     o::::o  n::::n    n::::nd:::::d     d:::::d u::::u    u::::u   i::::i      t:::::t
C:::::C              o::::o     o::::o  n::::n    n::::nd:::::d     d:::::d u::::u    u::::u   i::::i      t:::::t
C:::::C       CCCCCCo::::o     o::::o  n::::n    n::::nd:::::d     d:::::d u:::::uuuu:::::u   i::::i      t:::::t    tttttt
C:::::CCCCCCCC::::Co:::::ooooo:::::o  n::::n    n::::nd::::::ddddd::::::ddu:::::::::::::::uui::::::i     t::::::tttt:::::t
CC:::::::::::::::Co:::::::::::::::o  n::::n    n::::n d:::::::::::::::::d u:::::::::::::::ui::::::i     tt::::::::::::::t
CCC::::::::::::C oo:::::::::::oo   n::::n    n::::n  d:::::::::ddd::::d  uu::::::::uu:::ui::::::i       tt:::::::::::tt
CCCCCCCCCCCCC   ooooooooooo     nnnnnn    nnnnnn   ddddddddd   ddddd    uuuuuuuu  uuuuiiiiiiii         ttttttttttt

</pre>

</body>
</html>

Now, we will run our frontend container.

docker run -p 8000:80 --network bridge com.conduitapp/frontend

You can see your web app running at http://localhost:8000

But, I use docker-compose

I couldn’t get the docker-compose setup working completely. The whole environment boots up but ports are not forward to the local machine. So, from my MacBook I am unable to access the app. If I go inside the VM I am able to do that.

You will have to install podman-compose first.

pip3 install podman-compose

Now, from inside the conduit-api directory run the podman-compose up command. The whole setup boots up cleanly but as mentioned above I can’t access it from my local machine. If you are aware of the solution please share it with me. I will update this article.

Connecting to Podman Unix Socket REST API

To make it work we have to forward Unix socket on local machine.

ssh -nNT -L/tmp/podman.sock:/run/user/1000/podman/podman.sock ssh://core@localhost:54766

You can check connection URL by running following command.

podman system connection list
Name                         Identity                                          URI
podman-machine-default*      /Users/shekhargulati/.ssh/podman-machine-default  ssh://core@localhost:54766/run/user/1000/podman/podman.sock
podman-machine-default-root  /Users/shekhargulati/.ssh/podman-machine-default  ssh://root@localhost:54766/run/podman/podman.sock

Set DOCKER_HOST

export DOCKER_HOST=unix:///tmp/podman.sock

Now, you can use the API.

Conclusion

There are still rough edges in Podman so it will take some time before those are ironed out. It is not a 1-1 replacement yet. It requires tweaking things. I hope above the guide will help you migrate from Docker in case you take that path.

2 thoughts on “Podman for Java Developers: The Missing Tutorial”

  1. Need changes at multiple places in order to switch from docker to podman. I am curious about to know will it affect on k8s side as well.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: