Migrating to Podman
Docker is among the best-known container runtimes and probably only a few people (and sadly that didn’t include me) considered some of the other available options. When Kubernetes announced they want to phase-out the Dockershim, there was a bit of an uproar, but it probably took the recent license change for people to really want to move on.
I see these signals as a great opportunity for other runtimes to get more attention and also think the time is right to give them a try. One of them, Podman has been on my list for a pretty long time, but due to their limitation to run on Linux systems only, this poor macOS user never had the chance to really look into it. Gladly, they’ve found a workaround with Qemu - so let us give it a spin now.
I am currently working on a showcase for logging-vs-tracing, which needs a bit of infrastructure components, so this sounds like a nice exercise: In this post we are going to migrate from Docker and docker-compose to something that works properly with Podman.
And for completeness, here is the docker-compose file upfront:
Podman &
Before we start with the actual infrastructure services, let us talk a bit about Podman and cover the base installation and also some things that are good to know from the start.
Installation &
Podman comes with a pretty good install guide, the essential part for us is:
$ brew install podman
Right after the installation is complete we need to init and start our new machine:
$ podman machine init --memory=8192 --cpus=2
$ podman machine start
Is there an UI? &
If you come from Docker for Desktop you will probably wonder if there is some kind of UI.
podman-macos deals with all the nooks and crannies and provides some basic interface to start and stop the machine and also a limited control over running containers. It works perfectly, but one of my personal problems with Docker for Desktop is I am so used to rely on the UI, I never got fluent in using it from the CLI, which actually hurts my heart as a long time console user.
Can I compose? &
Although I’ve made the decision to run all commands in this post via CLI, there still is the open question if there is something like docker-compose for Podman.
podman-compose can help to translate your docker-compose file to the commands required for Podman, but has some drawbacks:
-
It directly runs all of them and mixes the outputs.
-
It starts all containers in a new pod.
-
Ports of the container aren’t properly exposed to the host.
-
And why the heck are people still using absolute paths in a shebang? Please use env.
-
The output is quite messy, if you are used to something like lazydocker.
Unfortunately there isn’t support for Podman in lazydocker yet: https://github.com/jesseduffield/lazydocker/issues/4 |
What about networking? &
Naturally, containers can be started with different privileges and this greatly impacts how networking is handled internally in default configuration:
-
In rootfull mode (run as root), a network bridge connects the containers in a virtual network, so they can access each other and the outside network (like the internet) via NAT. This also allows to bind container ports to privileged host ports. (<1024)
-
In rootless mode (run as user), a tap device is created to connect the container’s network with the usermode TCP/IP-stack, which unfortunately completely isolates containers from each other. The only way to access others containers is via host ports.
What is a pod? &
The concept of a pod was originally introduced within Kubernetes:
Pods are the smallest, most basic deployable objects in Kubernetes. A Pod represents a single instance of a running process in your cluster. Pods contain one or more containers, such as Docker containers. When a Pod runs multiple containers, the containers are managed as a single entity and share the Pod’s resources.
So generally speaking, a pod is a group of containers, that share the same network, process id and also the ipc namespaces and this is true for both Podman and Kubernetes.
Due to this conceptual resemblance it shoudln’t surprise anyone, that we can easily convert a Podman pod to something, that can be deployed directly to Kubernetes:
$ podman generate kube my-pod >> my-pod.yaml
How can I start a container? &
If you need to start a single container either rootfull or rootless should work, so let us focus on examples with and without a pod.
Rootless without a pod &
$ podman run -dt -p 8080:80 nginx
21ee82cae8b8e0e2744426c3a2f57d7274779b71370e242532e26d3e301124ca
Once the container is running you can reach it with curl:
$ curl -s localhost:8080 | htmlq --text h1
Welcome to nginx!
Rootless within a pod &
Let us do the same again, but this time within a pod. The first thing we have to do is to create the actual pod:
$ podman pod create -n mypod -p 8080:80
41983bfdf2e1c13d209cf9d114abe6dc298fffc24b7385d353edabbbc9890792
This looks good, let us see what we’ve got:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Ports}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS PORTS NAMES PODNAME
67b89dbd6e21 k8s.gcr.io/pause:3.5 Created 0.0.0.0:8080->80/tcp b6548bd64e31-infra mypod
Noteworthy here is we need to publish the ports on pod-level and that Podman creates an infrastructure container for us.
$ podman run -dt --pod mypod nginx
e2182dec80aa1fb42a06a01337fe86e951b13d89f9b600c50b39678d25a24301
Hint: There is handy shortcut to directly start a container in a new pod:
$ podman run -dt --pod new:mypod -p 8080:80 nginx
309d7f33bf472d790a13cc1a1cc7fff432d026e4c26c3844731b5c448b1b100a
In a previous section about [networking](#what-about-networking), I’ve mentioned that containers are isolated in this mode, here you can see it in action:
$ curl localhost:8080
curl: (7) Failed to connect to localhost port 8080: Connection refused
This can be avoided by creating a new network or by just using the bridge:
$ podman run -dt --pod new:mypod -p 8080:80 --network bridge nginx
54d6d488edad06477286e579fd255981761e5881b0d9a5eda1d5d7a14c016559
And just for the sake of completeness:
$ curl -s localhost:8080 | htmlq --text h1
Welcome to nginx!
How can I build a container? &
Building container is also piece of cake. Podman uses buildah (or rather code from it) to actually build the container, which is better explained here. The interesting part for us is that Dockerfile is also supported and heads up to a quick and pointless example:
FROM nginx
$ podman build --format docker -t mynginx .
STEP 1/1: FROM nginx
COMMIT mynginx
--> ea335eea17a
Successfully tagged localhost/mynginx:latest
Successfully tagged docker.io/library/nginx:latest
ea335eea17ab984571cd4a3bcf90a0413773b559c75ef4cda07d0ce952b00291
Equipped with this we should be able to start our services now.
Services &
In this section we are going to create each service from my docker-compose file and start it via Podman. Since we are doing it manually, we ignore the dependencies between the services and just start everything in correct order.
Create a pod &
No surprises here: We need a new pod, which also does the port handling on our bridge:
$ podman pod create -n logtrace --network bridge -p 6831:6831/udp -p 16686:16686 \
-p 9200:9200 -p 9300:9300 -p 12201:12201/udp -p 5601:5601 -p 9092:9092
ee627e6718c19e707eb03c97b5cf86e8280c91cce9b031fea000ff180fac3c28
A quick check if everything is well:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
Deploy Jaeger &
# Install jaeger
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "6831:6831/udp"
- "16686:16686"
This is going to be easy:
$ podman run -dit --name jaeger --pod=logtrace jaegertracing/all-in-one:latest
7f5a083ece1ee60e9d8b394bf25bd361aa98afa987a6840f0d5b2b5929b44b72
Checking time:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
7f5a083ece1e docker.io/jaegertracing/all-in-one:latest Up 3 days ago jaeger logtrace
Deploy Elastic &
# Install elastic
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.8.2
ports:
- "9200:9200"
- "9300:9300"
environment:
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
Besides the environment there is also no magic involved:
$ podman run -dit --name elastic --pod=logtrace -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
docker.elastic.co/elasticsearch/elasticsearch:7.14.2
2d81acbf527a3f2c26b4c66133b4826c460f719124d2ff1d71005127994c77a7
Checking time:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
7f5a083ece1e docker.io/jaegertracing/all-in-one:latest Up 3 days ago jaeger logtrace
2d81acbf527a docker.elastic.co/elasticsearch/elasticsearch:7.14.2 Exited (78) 3 days ago elastic logtrace
Something obviously went wrong. Unfortunate, but let us check what is wrong here:
$ podman logs 2d81acbf527a | grep -A 2 ERROR
ERROR: [1] bootstrap checks failed
[1] max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
Looks like we have some problems within Qemu, that didn’t happen in Docker. The problem is well explained here - including a solution:
Even easier than dealing with systcl
inside of a container, let us just bump to the current
version of elasticsearch, which seems to ignore this error altogether:
$ podman rm 2d81acbf527a
2d81acbf527a
$ podman run -it --name elastic --pod=logtrace -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" docker.elastic.co/elasticsearch/elasticsearch:7.14.2
847f303ffa7562778ea8b15fb83f8a6f6beec949af78edfc31f060a1cb50469b
Checking time:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
7f5a083ece1e docker.io/jaegertracing/all-in-one:latest Up 3 days ago jaeger logtrace
847f303ffa75 docker.elastic.co/elasticsearch/elasticsearch:7.14.2 Up 3 days ago elastic logtrace
Deploy Fluentd &
# Install fluentd
fluentd:
build: .
ports:
- "12201:12201/udp"
volumes:
- source: ./fluentd
target: /fluentd/etc
type: bind
depends_on:
- elasticsearch
Next on our list is fluentd. For this service we need to mount and bind a host path into the running container. Unfortunately, this is no easy task on macOS and there is a pending issue:
Alas, we don’t need to be able to change the config on-the-fly, copying the config directly into the container also does the trick here. So we are going to change the Dockerfile from my project a bit here:
FROM fluent/fluentd:v1.14-debian-1
USER root
COPY ./fluentd/fluent.conf /fluentd/etc/fluent.conf
RUN ["gem", "install", "fluent-plugin-elasticsearch"]
RUN ["gem", "install", "fluent-plugin-input-gelf"]
USER fluent
$ cd docker
$ podman build --format docker -t fluent .
STEP 1/6: FROM fluent/fluentd:v1.14-debian-1
STEP 2/6: USER root
STEP 3/6: COPY ./fluentd/fluent.conf /fluent/etc/fluent.conf
STEP 4/6: RUN ["gem", "install", "fluent-plugin-elasticsearch"]
STEP 5/6: RUN ["gem", "install", "fluent-plugin-input-gelf"]
STEP 6/6: USER fluent
..
Successfully tagged localhost/fluent:latest
215d4b1979f362ec4ce38c4ef57da8e16c3261d7060f07ec403e2d86941c6c61
And after that we just need to start the container:
$ podman run -dit --name fluent --pod=logtrace fluent
a76a5ecb32efb2ef5d22447d1cacce369ef6639afaadd3a8f41b1b6653c01852
Checking time again:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
7f5a083ece1e docker.io/jaegertracing/all-in-one:latest Up 3 days ago jaeger logtrace
847f303ffa75 docker.elastic.co/elasticsearch/elasticsearch:7.14.2 Up 3 days ago elastic logtrace
a76a5ecb32ef localhost/fluent:latest Up 3 days ago fluent logtrace
Deploy Kibana &
# Kibana
kibana:
image: docker.elastic.co/kibana/kibana-oss:6.8.2
ports:
- "5601:5601"
depends_on:
- elasticsearch
I think you get it and know the drill. The only thing we need to take care of is the hostname of elasticsearch, since networking works a bit different in Podman:
$ podman run -dit --name kibana --pod=logtrace -e "ELASTICSEARCH_HOSTS=http://localhost:9200" \
docker.elastic.co/kibana/kibana:7.14.2
cad125873b438efea4b549e51edc00981bf88bb3ed78c8bdf54aecb43fba64d8
More checking time:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
7f5a083ece1e docker.io/jaegertracing/all-in-one:latest Up 3 days ago jaeger logtrace
847f303ffa75 docker.elastic.co/elasticsearch/elasticsearch:7.14.2 Up 3 days ago elastic logtrace
a76a5ecb32ef localhost/fluent:latest Up 3 days ago fluent logtrace
cad125873b43 docker.elastic.co/kibana/kibana:7.14.2 Up 3 days ago kibana logtrace
Deploy Redpanda &
# Install redpanda
redpanda:
container_name: redpanda
image: vectorized/redpanda
hostname: redpanda
ports:
- "9092:9092"
One more - last time - promised:
$ podman run -dit --name redpanda --pod=logtrace vectorized/redpanda
b728da318549cca15ddd0019eec1cddff4e3e388cacbc0dcc1f7ea38480c81fc
And final checking time:
$ podman ps -a --pod --format "table {{.ID}} {{.Image}} {{.Status}} {{.Names}} {{.PodName}}"
CONTAINER ID IMAGE STATUS NAMES PODNAME
443c40c601ee k8s.gcr.io/pause:3.5 Up 3 days ago ee627e6718c1-infra logtrace
7f5a083ece1e docker.io/jaegertracing/all-in-one:latest Up 3 days ago jaeger logtrace
847f303ffa75 docker.elastic.co/elasticsearch/elasticsearch:7.14.2 Up 3 days ago elastic logtrace
a76a5ecb32ef localhost/fluent:latest Up 3 days ago fluent logtrace
cad125873b43 docker.elastic.co/kibana/kibana:7.14.2 Up 3 days ago kibana logtrace
b728da318549 docker.io/vectorized/redpanda:latest Up 3 days ago redpanda logtrace
Conclusion &
Podman is a nice replacement for Docker, but not every workflow and especially not every docker-compose file works out of the box. Network handling is quite different, but that might just be true on macOS.
While writing this post I enjoyed playing with it, learning the commands and also the way this can be scripted and added some handy aliases to my zsh file like this goodie:
$ eval `podman ps -a | fzf --multi --tac --no-sort | cut -d ' ' -f1 | sed -nE "s#(.*)#-l \'podman logs -f \1\'#gp" | xargs -r -0 -n10 -d'\n' echo multitail -C`
This basically displays the running container via fzf, allows multiselect and displays logs of the selected container in multitail.
I never did something like this with Docker, would have saved me quite some headaches I suppose.
My logging-vs-tracing showcase can be found here: