Skip to main content

Moving from docker-compose to Podman pods

Follow the migration of a virtual machine from Docker to Podman.
Image
Moving from docker compose to podman pods
Image by skeeze from Pixabay

It feels like forever since I wrote my Red Hat Enterprise Linux 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 the Digital Ocean Droplet that hosts my sites from the CentOS 7 Docker platform to the CentOS 8 Podman platform. I would really love to do something more sophisticated, but to be honest, a single host running containers is all I need. So, I set out to move from the Docker Compose world to Podman.

docker-compose alternatives still emerging

I used Docker Compose for one simple reason: I can define my applications in a portable and easy-to-write YAML file, which gives 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. Those are two containers, and managing them separately seems silly. Instead, I grouped them in a Docker Compose file. This solution 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 replacing Docker with Podman. Podman does not have a counterpart to the docker-compose command. 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 a Google search for the "Podman counterpart to docker-compose " would get 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 their own namespace, network, and security context. You can even start and stop the whole pod at once. The only thing it doesn't get me is a clean YAML file to define my services.

Note: I found this great example from Red Hat on pods, and 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. It sounds like the solution for me. I spent some time trying to learn how to write these things and eventually came across the Kompose tool. Kompose converts docker-compose files into Kubernetes definitions. I thought it was 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 lets you generate Kubernetes definitions from the existing runtime. For example, if you have a running container, you can use podman generate to create a YAML file to define that container. You can also do that with a pod. So, I manually 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. That's true in Podman as well—except when you're running inside 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 each other over the local host, and external networking reaches the pod, not the containers directly. When you run containers in a pod, you need to map ports on the pod like you would on the container in Docker or docker-compose. I also found that, although 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. The problems were mainly centered around SELinux. I will likely circle back and try to re-do all of this without superuser privileges.

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 command creates a pod with port 8080 mapped to inside port 80. If you then spin up a container listening on port 80, you'd have connectivity.

Create a container in the pod

To create a container in the pod, use podman run, but 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 container 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

Notice that I pointed the wordpress_db_host in the env: to localhost. That's because the WordPress container is going to find the database container on the local host. Like magic. Our pod has three containers. Yes, I ran two, but the third 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

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

Image
The initial wordpress setup page

The initial WordPress setup page is displayed. Success.

We're done, right? Nope. Now we want to make a YAML file that defines all of this.

Generate the YAML for our pod

We output our pod as a YAML definition by using podman generate kube command:

[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 deleted a bunch of the env: entries 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 editing:

# 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 can directly copy and paste the above content into your own file, and then use Podman to bring up exactly the same pod.

Bring up a pod from the YAML

Next, use podman play kube to start your pod 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 WordPress setup page, just like before.

Conclusion

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

[ Getting started with containers? Check out this free course. Deploying containerized applications: A technical overview. ]

Topics:   Containers   Podman  
Author’s photo

Nathan Lager

Nate is a Technical Account Manager with Red Hat and an experienced sysadmin with 20 years in the industry.  He first encountered Linux (Red Hat 5.0) as a teenager, after deciding that software licensing was too expensive for a kid with no income, in the late 90’s.  Since then he’s run More about me

Try Red Hat Enterprise Linux

Download it at no charge from the Red Hat Developer program.