moving from docker-compose to podman pods

It feels like forever since I wrote my RHEL 8 beta intro to podman. In fact, it’s been quite a while, and a lot has happened since then. For some time now I’ve been planning on moving my Digital Ocean droplet, that hosts this and a few other sites, from the Centos 7 Docker platform that it’s on now, into a Centos 8 Podman platform. I would really love to do something more sophisticated, but being honest, a single host running containers is really all I need. So, I set out to move from the docker-compose world to podman.

No Great docker-compose alternative

I used docker-compose for one simple reason. It allowed me to define my applications in a portable and easy to write yaml file, which gave me the ability to group each site with its dependencies. For instance, this site requires a web server running php, and a database to store its data in. Those are two containers, and managing them separately seemed silly. So I grouped them in a docker-compose file. This allows me to work with the whole stack when I want to do things like stop services, or pull in updates for the container images I chose to use.

Pods

Well, moving to Centos 8 meant losing docker, and replacing it with podman. Podman.. Does not have a docker-compose counterpart. Well, it does, sort of. There’s a project in the works called podman-compose, which is supposed to do the same basic thing as docker-compose. I wanted to find the “right” solution though. Honestly, that was not an easy task. You’d think that doing a google search for the podman counterpart to docker-compose would get you an article about how podman replaces that functionality with something else. I couldn’t find anything though. What I did find was a reference to Pods in podman. Pods are a way of grouping containers together inside of their own namespace, network, and security context. You can even start and stop the whole pod at once. Sounds great! The only thing it doesn’t get me is a clean yaml file to define my services in. Also worth noting, I found this great expample from Red Hat on pods, it even touches on networking. Podman: Managing pods and containers in a local container runtime

Podman play

Podman does, however, let you import kubernetes definitions using the podman play command. Kubernetes definitions are yaml! Sounds like the solution for me! I spent some time trying to learn how these things are written, and eventually came across the “kompose” tool. Kompose is meant to convert your docker-compose files into Kubernetes definitions! PERFECT! except it wasn’t. What I needed specifically was a pod definition, and that’s not what Kompose gave me. I might have been able to make a pod definition out of what I had, but I had another hunch.

Podman generate

Podman will let you generate kube definitions from existing runtime. Like, if you had a container running, you could podman generate a yaml file to define that container. You can also do that with a pod! So I maually defined one of my wordpress sites in podman. Here are a few notes on that process.

Mapping ports

In the docker world, ports are mapped to containers, and that’s true in podman as well. Except when you’re running insdie a pod. See, the pod is like a container of containers. Networking within the pod is more similar to networking within a host OS. Pods reach eachother over localhost, and external networking reaches the pod, not the containers themselves directly. So when you intend to run containers in a pod, you need to map ports on the pod like you would have on the container in docker or docker-compose. I also found that, regardless of the fact that one of the benefits to podman is the ability to run as a standard user… I had to do all of this as root because of some security problems I ran into when I created the pods. Mainly selinux. So I ended up doing all of his as root, like you would have with Docker. I will likely circle back and try to re-do all of this without superuser privs.

So let’s create a pod

[gangrif@batou-lan ~]$ sudo podman pod create --name my-pod -p 8080:80
850425b9c02dc438a04c278196ef645fc9b8a27070a80d2c1d53aca0f1730502
[gangrif@batou-lan ~]$ sudo podman pod ls
POD ID         NAME     STATUS    CREATED         # OF CONTAINERS   INFRA ID
850425b9c02d   my-pod   Created   7 seconds ago   1                 b525a0511d3e

This creates a pod, that has port 8080 mapped to inside port 80. So if you were to then spin up a container listening on port 80, you’d have connectivity.

Creating a container, in the pod

Now to create a container in the pod, you’d use podman run, like you’d expect, but you don’t map a port. This makes more sense when you have more than one container to work with, so I’m going to create a database, and then a wordpress container.

[gangrif@batou-lan ~]$ sudo podman run \
-d --restart=always --pod=my-pod \
-e MYSQL_ROOT_PASSWORD="myrootpass" \
-e MYSQL_DATABASE="wp" \
-e MYSQL_USER="wordpress" \
-e MYSQL_PASSWORD="w0rdpr3ss" \
--name=wptest-db mariadb
Trying to pull registry.fedoraproject.org/mariadb...
  manifest unknown: manifest unknown
Trying to pull registry.access.redhat.com/mariadb...
  name unknown: Repo not found
Trying to pull registry.centos.org/mariadb...
  manifest unknown: manifest unknown
Trying to pull docker.io/library/mariadb...
Getting image source signatures
Copying blob 42ed51adaf49 done  
Copying blob 127c9761dcba done  
Copying blob 7e2d48f22ade done  
Copying blob a4a2a29f9ba4 done  
Copying blob 4039240d2e0b done  
Copying blob d13bf203e905 done  
Copying blob 6518a50ecb7c done  
Copying blob b5bc5a5c2503 done  
Copying blob 67412c7f89bc done  
Copying blob 58175d975ba9 done  
Copying blob 0c6efbafd3cb done  
Copying blob 1e18725209e8 done  
Copying blob 05202eb0846d done  
Copying config 22851c7fe9 done  
Writing manifest to image destination
Storing signatures
fed9756de1017a0ab38ff4b687a854af5422ec2c9fd13e175a78283426ccfc04

[gangrif@batou-lan ~]$ sudo podman run \
-d --restart=always --pod=my-pod \
-e WORDPRESS_DB_NAME="wp" \
-e WORDPRESS_DB_USER="wordpress" \
-e WORDPRESS_DB_PASSWORD="w0rdpr3ss" \
-e WORDPRESS_DB_HOST="127.0.0.1" \
--name wptest-web wordpress
Trying to pull registry.fedoraproject.org/wordpress...
  manifest unknown: manifest unknown
Trying to pull registry.access.redhat.com/wordpress...
  name unknown: Repo not found
Trying to pull registry.centos.org/wordpress...
  manifest unknown: manifest unknown
Trying to pull docker.io/library/wordpress...
Getting image source signatures
Copying blob f54006e0dc29 done  
Copying blob e0d3d1244592 done  
Copying blob eb2d00c10344 done  
Copying blob 8559a31e96f4 done  
Copying blob e0276193a084 done  
Copying blob 3a60f364b0c5 done  
Copying blob 8faf60068506 done  
Copying blob c59965a5777f done  
Copying blob 42c09ef39fe7 done  
Copying blob db37570cfdf4 done  
Copying blob 3e309988c00b done  
Copying blob 2c289722ebb3 done  
Copying blob e80a84c5a269 done  
Copying blob 491a234e2c26 done  
Copying blob b83f4c8507f7 done  
Copying blob 944a23d0ea39 done  
Copying blob a650de05eb1e done  
Copying blob a7780c30584c done  
Copying blob 267943a2fe25 done  
Copying blob ed59c3cd6acc done  
Copying config a5fef19f5a done  
Writing manifest to image destination
Storing signatures
a49711540329f4307ff218c277bb5a2848b37bf6ded922dfd2a839c564ddbdf1

Now, notice that I pointed the wordpress_db_host in the env to localhost. That’s because the WP container is going to find the DB container on localhost. Like magic. Now we can see our pod has 3 containers. Yes, I ran two, but one of them is the container that does the pod magic.

[gangrif@batou-lan ~]$ sudo podman pod ls
POD ID         NAME     STATUS    CREATED          # OF CONTAINERS   INFRA ID
850425b9c02d   my-pod   Running   12 minutes ago   3                 b525a0511d3e

And, as expected, podman ps gives us two containers.

[gangrif@batou-lan ~]$ sudo podman ps
CONTAINER ID  IMAGE                               COMMAND               CREATED        STATUS            PORTS                 NAMES
a49711540329  docker.io/library/wordpress:latest  apache2-foregroun...  2 minutes ago  Up 2 minutes ago  0.0.0.0:8080->80/tcp  wptest-web
fed9756de101  docker.io/library/mariadb:latest    mysqld                5 minutes ago  Up 5 minutes ago  0.0.0.0:8080->80/tcp  wptest-db

And, in my browser, I can get to the WordPress setup page in my container, via localhost:8080

wordpress setup page
The initial wordpress setup page. Success!

So we’re done, right? Nope, Now we want to make a yaml file that defines all of this.

Generating the yaml for our pod

Now we can use podman generate to output our pod as a yaml definition using podman generate kube.

[gangrif@batou-lan ~]$ sudo podman generate kube my-pod >> my-pod.yaml
[gangrif@batou-lan ~]$ cat my-pod.yaml 
# Generation of Kubernetes YAML is still under development!
#
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-1.9.3
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2020-07-01T20:17:42Z"
  labels:
    app: my-pod
  name: my-pod
spec:
  containers:
  - command:
    - apache2-foreground
    env:
    - name: PATH
      value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    - name: TERM
      value: xterm
    - name: HOSTNAME
      value: my-pod
    - name: PHP_MD5
    - name: PHP_VERSION
      value: 7.4.7
    - name: PHPIZE_DEPS
      value: "autoconf \t\tdpkg-dev \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake
        \t\tpkg-config \t\tre2c"
    - name: APACHE_CONFDIR
      value: /etc/apache2
    - name: PHP_ASC_URL
      value: https://www.php.net/distributions/php-7.4.7.tar.xz.asc
    - name: PHP_EXTRA_BUILD_DEPS
      value: apache2-dev
    - name: PHP_CFLAGS
      value: -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
    - name: WORDPRESS_VERSION
      value: 5.4.2
    - name: WORDPRESS_DB_NAME
      value: wp
    - name: WORDPRESS_DB_HOST
      value: 127.0.0.1
    - name: PHP_LDFLAGS
      value: -Wl,-O1 -pie
    - name: APACHE_ENVVARS
      value: /etc/apache2/envvars
    - name: WORDPRESS_DB_USER
      value: wordpress
    - name: PHP_CPPFLAGS
      value: -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
    - name: PHP_URL
      value: https://www.php.net/distributions/php-7.4.7.tar.xz
    - name: PHP_INI_DIR
      value: /usr/local/etc/php
    - name: WORDPRESS_DB_PASSWORD
      value: w0rdpr3ss
    - name: PHP_EXTRA_CONFIGURE_ARGS
      value: --with-apxs2 --disable-cgi
    - name: PHP_SHA256
      value: 53558f8f24cd8ab6fa0ea252ca8198e2650160649681ce5230c1df1dc2b52faf
    - name: WORDPRESS_SHA1
      value: e5631f812232fbd45d3431783d3db2e0d5670d2d
    - name: GPG_KEYS
      value: 42670A7FE4D0441C8E4632349E4FDC074A4EF02D 5A52880781F755608BF815FC910DEB46F53EA312
    - name: container
      value: podman
    image: docker.io/library/wordpress:latest
    name: wptest-web
    ports:
    - containerPort: 80
      hostPort: 8080
      protocol: TCP
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
      seLinuxOptions: {}
    workingDir: /var/www/html
  - command:
    - mysqld
    env:
    - name: PATH
      value: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    - name: TERM
      value: xterm
    - name: HOSTNAME
      value: my-pod
    - name: MARIADB_MAJOR
      value: "10.5"
    - name: MARIADB_VERSION
      value: 1:10.5.4+maria~focal
    - name: MYSQL_ROOT_PASSWORD
      value: myrootpass
    - name: MYSQL_USER
      value: wordpress
    - name: GOSU_VERSION
      value: "1.12"
    - name: GPG_KEYS
      value: 177F4010FE56CA3336300305F1656F24C74CD1D8
    - name: MYSQL_PASSWORD
      value: w0rdpr3ss
    - name: MYSQL_DATABASE
      value: wp
    - name: container
      value: podman
    image: docker.io/library/mariadb:latest
    name: wptest-db
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
      seLinuxOptions: {}
    workingDir: /
status: {}

I found that this output needed some cleanup. I ended up deleting a bunch of the env that I thought the container images would easily re-propagate, and in fact could cause conflicts if I were to build this again from scratch. Here’s what I ended up with after some clean-up.

# Generation of Kubernetes YAML is still under development!
#
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-1.9.3
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2020-07-01T20:17:42Z"
  labels:
    app: my-pod
  name: my-pod
spec:
  containers:
  - name: wptest-web
    env:
    - name: WORDPRESS_DB_NAME
      value: wp
    - name: WORDPRESS_DB_HOST
      value: 127.0.0.1
    - name: WORDPRESS_DB_USER
      value: wordpress
    - name: WORDPRESS_DB_PASSWORD
      value: w0rdpr3ss
    image: docker.io/library/wordpress:latest
    ports:
    - containerPort: 80
      hostPort: 8080
      protocol: TCP
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
      seLinuxOptions: {}
    workingDir: /var/www/html
  - name: wptest-db
    env:
    - name: MYSQL_ROOT_PASSWORD
      value: myrootpass
    - name: MYSQL_USER
      value: wordpress
    - name: MYSQL_PASSWORD
      value: w0rdpr3ss
    - name: MYSQL_DATABASE
      value: wp
    image: docker.io/library/mariadb:latest
    resources: {}
    securityContext:
      allowPrivilegeEscalation: true
      capabilities: {}
      privileged: false
      readOnlyRootFilesystem: false
      seLinuxOptions: {}
    workingDir: /
status: {}

You could directly copy and paste that into your own file, and then use podman to bring up the exact same pod.

Bringing up a pod from the yaml

Now, you can use podman play kube to bring your pod up from the defined yaml!

[gangrif@batou-lan ~]$ sudo podman pod ls
[gangrif@batou-lan ~]$ sudo podman ps 
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
[gangrif@batou-lan ~]$ sudo podman ps -a
CONTAINER ID  IMAGE  COMMAND  CREATED  STATUS  PORTS  NAMES
[gangrif@batou-lan ~]$ sudo podman play kube ./my-pod.yaml 
Trying to pull docker.io/library/wordpress:latest...
Getting image source signatures
Copying blob 3a60f364b0c5 skipped: already exists  
Copying blob e0276193a084 skipped: already exists  
Copying blob 8559a31e96f4 skipped: already exists  
Copying blob f54006e0dc29 skipped: already exists  
Copying blob eb2d00c10344 skipped: already exists  
Copying blob e0d3d1244592 skipped: already exists  
Copying blob 8faf60068506 skipped: already exists  
Copying blob 3e309988c00b skipped: already exists  
Copying blob 42c09ef39fe7 skipped: already exists  
Copying blob c59965a5777f skipped: already exists  
Copying blob 2c289722ebb3 skipped: already exists  
Copying blob db37570cfdf4 skipped: already exists  
Copying blob 491a234e2c26 skipped: already exists  
Copying blob 944a23d0ea39 skipped: already exists  
Copying blob b83f4c8507f7 skipped: already exists  
Copying blob e80a84c5a269 skipped: already exists  
Copying blob 267943a2fe25 [--------------------------------------] 0.0b / 0.0b
Copying blob a7780c30584c [--------------------------------------] 0.0b / 0.0b
Copying blob ed59c3cd6acc [--------------------------------------] 0.0b / 0.0b
Copying blob a650de05eb1e [--------------------------------------] 0.0b / 0.0b
Copying config a5fef19f5a done  
Writing manifest to image destination
Storing signatures
Trying to pull docker.io/library/mariadb:latest...
Getting image source signatures
Copying blob 42ed51adaf49 skipped: already exists  
Copying blob 127c9761dcba skipped: already exists  
Copying blob 4039240d2e0b skipped: already exists  
Copying blob 1e18725209e8 skipped: already exists  
Copying blob d13bf203e905 skipped: already exists  
Copying blob 6518a50ecb7c skipped: already exists  
Copying blob 67412c7f89bc skipped: already exists  
Copying blob 7e2d48f22ade skipped: already exists  
Copying blob a4a2a29f9ba4 skipped: already exists  
Copying blob 58175d975ba9 skipped: already exists  
Copying blob 0c6efbafd3cb skipped: already exists  
Copying blob b5bc5a5c2503 [--------------------------------------] 0.0b / 0.0b
Copying config 22851c7fe9 done  
Writing manifest to image destination
Storing signatures
Pod:
a91dc8859e85505b43f82f1be248880f76138f0ae86f12c3ffdea4d309d4eacf
Containers:
87a67588fbacf959c95f8f63f9f26ffa7b4fb3c7f8827aadd65bcd94de526e37
cc3f3e7255c8110826565673b5d30ebc223cbfcefa01597714231e1305382d3c
[gangrif@batou-lan ~]$ sudo podman pod ls
POD ID         NAME     STATUS    CREATED          # OF CONTAINERS   INFRA ID
a91dc8859e85   my-pod   Running   10 seconds ago   3                 aa53c9308991
[gangrif@batou-lan ~]$ sudo podman ps
CONTAINER ID  IMAGE                               COMMAND               CREATED         STATUS             PORTS                 NAMES
cc3f3e7255c8  docker.io/library/mariadb:latest    docker-entrypoint...  12 seconds ago  Up 11 seconds ago  0.0.0.0:8080->80/tcp  wptest-db
87a67588fbac  docker.io/library/wordpress:latest  docker-entrypoint...  13 seconds ago  Up 12 seconds ago  0.0.0.0:8080->80/tcp  wptest-web

And if you hit up port 8080, you should get the WP setup page, just as before.

Conclusion

So, I hope this was helpful! It took me some time to figure out, so I thought it was worth sharing! Happy podmanning!