# From Podman to Kubernetes: A Practical Integration Guide

Podman is a lightweight container engine that provides an easy-to-use
command-line interface for managing images and containers. It is often used as a
[drop-in replacement for Docker](https://betterstack.com/community/guides/scaling-docker/podman-vs-docker/) due to the fact that,
excluding Docker Swarm commands, its CLI is fully compatible with the Docker
CLI.

However, Podman's capabilities extend beyond Docker compatibility, one of them
being Kubernetes integration (the ability to parse and generate Kubernetes
manifests). This feature offers additional convenience and flexibility, allowing
you to easily deploy and manage your Podman workloads in a Kubernetes cluster or
seamlessly transfer existing workloads from a Kubernetes cluster to a Podman
installation.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/mvWYdrn7m2c" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>

This guide aims to demonstrate how Podman and Kubernetes can be integrated to
leverage the benefits of both technologies in an efficient and practical manner.
We will go through a basic introduction to pods before diving into more advanced
topics and scenarios involving Kubernetes.

By the end of this article, you'll have a clear understanding of how Podman and
Kubernetes can be utilized together to optimize your container management
workflows and maximize the efficiency of your deployments.

Let's start with an overview of pods and how they're used in Podman.

## Prerequisites

- Good Linux command-line skills.
- Basic experience with Podman and Kubernetes.
- Recent version of
  [Podman installed on your system](https://podman.io/docs/installation).
- (Optional) [Docker Engine](https://docs.docker.com/engine/) installed on your
  system for running the [minikube](https://minikube.sigs.k8s.io/) examples.


[ad-logs]

## Understanding pods

As you know, the concept of pods doesn't exist in all container engines. For
instance, Docker doesn't support pods. Thus, many engineers are unaware of pods
and their use-cases and prefer working with individual containers instead.
However, with the increasing popularity of Kubernetes, it has become essential
for many users to understand and integrate pods into their containerization
workflows.

In Kubernetes, pods represent the smallest and simplest deployable objects,
consisting of one or more containers managed as a cohesive unit. Containers
within a pod can share resources like network and storage while maintaining
separate filesystems and process namespaces, ensuring tighter security and
better stability.

Podman aligns with this concept by allowing users to organize containers into
pods. While there are differences in the implementations of Kubernetes and
Podman, the core idea of managing containers as a unified entity remains
consistent, making Podman pods capable of performing similar tasks.

To create a new pod, you execute:

```command
podman pod create my-first-pod
```

This outputs a SHA-256 hash uniquely identifying the pod on your system:

```text
[output]
e22b6a695bd8e808cadd2c39490951aba29c971c7be83eacc643b11a0bdc4ec7
```

You can issue the following command to further confirm that the pod is created
successfully:

```command
podman pod ls
```

It produces a similar output:

```text
[output]
POD ID        NAME          STATUS      CREATED         INFRA ID      # OF CONTAINERS
e22b6a695bd8  my-first-pod  Created     23 seconds ago  131ee0bcd059  1
```

Let's examine each column:

- `POD ID` shows the unique identifier of the newly created pod. Upon closer
  examination, you'll notice that its value corresponds to the initial 12
  characters of the SHA-256 hash generated by the `podman pod create` command.
  You can use this ID to distinguish this pod in subsequent commands and
  operations.
- `NAME` indicates the name of the newly created pod. Most `podman` commands
  allow you to reference a pod by either its name or its ID interchangeably.
- `STATUS` indicates the state of the newly created pod, which can be one of
  `Created`, `Running`, `Stopped`, `Exited` or `Dead`. In this case, the status
  is `Created`, which means that the pod definition has been created, but no
  container processes are currently actively running inside.
- `CREATED` simply indicates how long ago the pod was created.
- `INFRA ID` is an interesting one. It shows the identifier of the
  infrastructure container that the pod was created with (in this case,
  `131ee0bcd059`). The infrastructure container is what allows containers
  running inside a pod to share various Linux namespaces. By default, Podman
  orchestrates the pod in a way that allows its containers to share the `net`,
  `uts`, and `ipc` namespaces. This allows containers within the pod to
  communicate with each other and re-use certain resources.
- `# OF CONTAINERS` shows the number of containers attached to the pod. A pod
  always starts with 1 container attached to it by default (the infrastructure
  container), even though its process is not started automatically, as you will
  see in a moment.

To examine the existing containers, type:

```command
podman container ps -a
```

The output shows the infrastructure container of the pod that you just created:

```text
[output]
CONTAINER ID  IMAGE                           COMMAND     CREATED         STATUS      PORTS       NAMES
131ee0bcd059  localhost/podman-pause:4.3.1-0              51 seconds ago  Created                 e22b6a695bd8-infra
```

Notice how the `CONTAINER ID` matches the `INFRA ID` of the created pod, and how
the first 12 characters of the container name, `e22b6a695bd8-infra`, match the
`POD ID`. These relationships are always true and make it very simple to
identify the infrastructure container for each pod on systems where several pods
might be running simultaneously.

When you create a new empty pod, the infrastructure container is prepared for
launch, but no process is actually started. Because of that, the container
initially shows as `Created` instead of `Running`, and the `-a` flag is required
for the `podman container ps` command to display it.

At this point, no namespaces have been established for the pod containers
either. Type in the following command to verify this:

```command
lsns -T
```

You will see a similar output:

```text
[output]
NS             TYPE   NPROCS   PID USER  COMMAND
4026531837     user        4 98786 marin /lib/systemd/systemd --user
├─4026531834   time        5 98786 marin /lib/systemd/systemd --user
├─4026531835   cgroup      5 98786 marin /lib/systemd/systemd --user
├─4026531836   pid         5 98786 marin /lib/systemd/systemd --user
├─4026531838   uts         5 98786 marin /lib/systemd/systemd --user
├─4026531839   ipc         5 98786 marin /lib/systemd/systemd --user
├─4026531840   net         5 98786 marin /lib/systemd/systemd --user
├─4026531841   mnt         4 98786 marin /lib/systemd/systemd --user
├─4026532336   mnt         0       root
└─4026532337   user        1 99106 marin catatonit -P
  └─4026532338 mnt         1 99106 marin catatonit -P
```

The `/lib/systemd/systemd --user` lines display the namespaces utilized by the
service manager that was initiated when you logged in to your user account on
the given Linux machine. The `catatonit -P` lines, on the other hand, display
the namespaces held by the global pause process that Podman maintains while you
interact with it in rootless mode. We won't delve into the details of why these
namespaces exist in the first place, but it's important to know that they are
there and that this is typically the standard `lsns` output that you will
observe even before a new pod has performed any actual work.

Let's add a container to the newly created pod and see what happens. For this
experiment, we'll use the
[hashicorp/http-echo](https://hub.docker.com/r/hashicorp/http-echo) image from
Docker Hub (`http-echo` is a small in-memory webserver commonly employed for
testing purposes):

```command
podman run -d --pod my-first-pod docker.io/hashicorp/http-echo:1.0.0
```

List the containers once again:

```command
podman container ps
```

This time both the infrastructure container and the `http-echo` container appear
to be `Running`:

```text
[output]
CONTAINER ID  IMAGE                                COMMAND     CREATED         STATUS             PORTS       NAMES
131ee0bcd059  localhost/podman-pause:4.3.1-0                   6 minutes ago   Up 23 seconds ago              e22b6a695bd8-infra
c57f4d354eb4  docker.io/hashicorp/http-echo:1.0.0              22 seconds ago  Up 23 seconds ago              gallant_wescoff
```

The pod is listed as `Running` as well:

```command
podman pod ps
```

```text
[output]
POD ID        NAME          STATUS      CREATED         INFRA ID      # OF CONTAINERS
e22b6a695bd8  my-first-pod  Running     7 minutes ago   131ee0bcd059  2
```

If you perform an `lsns` again, you'll notice several changes:

```command
lsns -T
```

```text
[output]
NS             TYPE   NPROCS    PID USER   COMMAND
4026531837     user        4  98786 marin  /lib/systemd/systemd --user
├─4026531834   time       10  98786 marin  /lib/systemd/systemd --user
├─4026531835   cgroup      8  98786 marin  /lib/systemd/systemd --user
├─4026531836   pid         8  98786 marin  /lib/systemd/systemd --user
├─4026531838   uts         8  98786 marin  /lib/systemd/systemd --user
├─4026531839   ipc         8  98786 marin  /lib/systemd/systemd --user
├─4026531840   net         8  98786 marin  /lib/systemd/systemd --user
├─4026531841   mnt         4  98786 marin  /lib/systemd/systemd --user
├─4026532336   mnt         0        root
└─4026532337   user        6  99106 marin  catatonit -P
  ├─4026532338 mnt         3  99106 marin  catatonit -P
[highlight]
  ├─4026532340 net         2 100589 marin  /catatonit -P
  ├─4026532401 mnt         1 100589 marin  /catatonit -P
  ├─4026532402 mnt         1 100584 marin  /usr/bin/slirp4netns --disable-host-loopback --mtu=65520 --enable-sandbox --enable-seccomp --enable-ipv6 -c -e 3 -r 4 --netns-type=path /run/user/1000/netns/netns-844a415e-435c-39aa-9962-b04eaf69e806 tap0
  ├─4026532403 uts         2 100589 marin  /catatonit -P
  ├─4026532404 ipc         2 100589 marin  /catatonit -P
  ├─4026532405 pid         1 100589 marin  /catatonit -P
  ├─4026532406 cgroup      1 100589 marin  /catatonit -P
  ├─4026532407 mnt         1 100594 165531 /http-echo
  ├─4026532408 pid         1 100594 165531 /http-echo
  └─4026532409 cgroup      1 100594 165531 /http-echo
[/highlight]
```

The `/catatonit -P` process (PID: `100589`) is the main process of the
infrastructure container. As you can see, it operates inside `net`, `mnt`,
`utc`, `ipc`, `pid`, and `cgroup` namespaces that are completely different from
the root namespaces (as indicated by the `systemd` process). The `/http-echo`
process, itself, runs in separate `mnt`, `pid` and `cgroup` namespaces, but
shares its `net`, `uts`, and `ipc` namespaces with the `catatonit` process in
the infrastructure container.

This may not be completely obvious at first, so to confirm this, you can also
run:

```command
lsns -T -p $(pgrep http-echo)
```

The output is clear:

```text
[output]
NS             TYPE   NPROCS    PID USER   COMMAND
4026531837     user        4  98786 marin  /lib/systemd/systemd --user
├─4026531834   time       10  98786 marin  /lib/systemd/systemd --user
└─4026532337   user        6  99106 marin  catatonit -P
  ├─4026532340 net         2 100589 marin  /catatonit -P
  ├─4026532403 uts         2 100589 marin  /catatonit -P
  ├─4026532404 ipc         2 100589 marin  /catatonit -P
  ├─4026532407 mnt         1 100594 165531 /http-echo
  ├─4026532408 pid         1 100594 165531 /http-echo
  └─4026532409 cgroup      1 100594 165531 /http-echo
```

- The `net`, `uts` and `ipc` namespaces are the same as the ones held by the
  infrastructure container.
- The `user` namespace is the same as the one held by the global pause process
  maintained by rootless Podman.
- The `time` namespace is the root `time` namespace.
- The `mnt`, `pid` and `cgroup` namespaces are unique to the `http-echo`
  container, isolating it from other containers in the pod.

This solidifies the idea that pods are essentially a group of containers capable
of sharing namespaces.

As I said earlier, pods also allow you to manage containers as one cohesive
unit. To see this in practice, type:

```command
podman pod stop my-first-pod
```

```text
[output]
e22b6a695bd8e808cadd2c39490951aba29c971c7be83eacc643b11a0bdc4ec7
```

This command stops the pod and all of its associated containers. To confirm
this, type:

```command
podman container ps -a
```

You will see that both containers were stopped:

```text
[output]
CONTAINER ID  IMAGE                                COMMAND     CREATED         STATUS                     PORTS       NAMES
131ee0bcd059  localhost/podman-pause:4.3.1-0                   25 minutes ago  Exited (0) 22 seconds ago              e22b6a695bd8-infra
c57f4d354eb4  docker.io/hashicorp/http-echo:1.0.0              19 minutes ago  Exited (2) 22 seconds ago              gallant_wescoff
```

The pod itself was stopped as well:

```command
podman pod ls
```

```text
[output]
POD ID        NAME          STATUS      CREATED         INFRA ID      # OF CONTAINERS
e22b6a695bd8  my-first-pod  Exited      28 minutes ago  131ee0bcd059  2
```

When you no longer need a pod, you can remove it completely by typing:

```command
podman pod rm my-first-pod
```

```text
[output]
e22b6a695bd8e808cadd2c39490951aba29c971c7be83eacc643b11a0bdc4ec7
```

This removes not only the pod, but also all of its associated containers.

You can verify this worked by repeating the `podman pod ls` and
`podman container ps -a` commands. You will see that neither pods nor containers
exist on your system:

```command
podman pod ls
```

```text
[output]
POD ID      NAME        STATUS      CREATED     INFRA ID    # OF CONTAINERS
```

```command
podman container ps -a
```

```text
[output]
CONTAINER ID  IMAGE       COMMAND     CREATED     STATUS      PORTS       NAMES
```

With that, you have covered the basics of working with Podman pods. Now, let's
explore their practical use through a real-world example.

## Exploring sidecar containers

Pods are often used for adding sidecar containers to an application. Sidecar
containers basically provide additional functionality and support to the main
application container. This supports use cases such as configuration management,
log shipping, role-based access control, and more.

To understand this better, let's explore a practical log shipping example, where
a web server logs incoming HTTP requests and a log shipper forwards them to an
external service for indexing. In this scenario, the application pod will
include two containers:

- A [Caddy](https://caddyserver.com/) container for serving web pages over HTTP.
- A [Vector](https://vector.dev/) container configured to ship logs from your
  web server to [Better Stack](https://betterstack.com/logs).

Create a new pod by typing:

```command
podman pod create --name example --publish 8080:80
```

```text
[output]
e21066fdb234833ffd3167a1b3bda8f5910df7708176da594a054dd09200fae
```

Note how the command looks slightly different compared to your previous
invocation of `podman pod create`.

First, you are using the `--name` option to specify the name of the pod. A name
can be provided to the `podman pod create` command by either using the `--name`
option or as the very last positional argument. In other words, the command
`podman pod create --publish 8080:80 example` is also perfectly valid and serves
the very same purpose, but for the sake of clarity, using `--name` when passing
multiple command-line options is usually a lot easier to read and comprehend.

Most importantly though, you specified the additional command-line option
`--publish 8080:80`. As you remember, we already established that containers
within a pod share the same network namespace by default. Therefore, if you want
to receive any web traffic, you need to expose port `8080` to the host for the
entire pod. You can't do it for just an individual container, as it shares its
network namespace with the other containers in the pod, and the network
namespace is configured when the pod is originally created. By using the
`--publish` option, you ensure that any traffic coming to port `8080` on the
host machine is going to be forwarded to port `80` within the pod, where the
Caddy container will be listening at.

Add Caddy to the pod by typing:

```command
podman create --pod example --name caddy docker.io/library/caddy:2.7.6-alpine
```

Here, through the `--pod example` option, you are specifying that you want
Podman to attach the container to an existing pod named `example` (the one that
you created earlier). You're also giving the container a specific name with the
`--name caddy` option. Finally, `docker.io/library/caddy:2.7.6-alpine` specifies
the precise image that the container should be created from.

Podman fulfills the request and produces the following output:

```text
[output]
Trying to pull docker.io/library/caddy:2.7.6-alpine...
Getting image source signatures
Copying blob b7343593237d done
Copying blob c926b61bad3b done
Copying blob 6fd2155878b9 done
Copying blob 08886dfc0722 done
Copying config 657b947906 done
Writing manifest to image destination
Storing signatures
7307f130b2951ea8202bbf6d1d6d1a81fbdb66d022d65c26f9c209ee2e664bf2
```

Keep in mind that the container's assigned name doesn't apply only to the
specific pod but is reserved globally.

If you try to create another container with the same name, you will get an
error, even though it's not running in the same pod:

```command
podman create --name caddy docker.io/library/caddy:2.7.6-alpine
```

```text
[output]
Error: creating container storage: the container name "caddy" is already in use by 7307f130b2951ea8202bbf6d1d6d1a81fbdb66d022d65c26f9c209ee2e664bf2. You have to remove that container to be able to reuse that name: that name is already in use
```

Now that the Caddy container has been created, it's interesting to see it in
action. Run the following command:

```command
curl localhost:8080
```

Surprisingly, it turns out that the web server is currently unreachable:

```text
[output]
curl: (7) Failed to connect to localhost port 8080 after 0 ms: Couldn't connect to server
```

Why is that? While the `podman create` command indeed creates the container and
attaches it to the `example` pod, it doesn't actually start its main process. If
you wish the process to start immediately after the container is created, you
should execute `podman run` instead of `podman create`, like this:

```command
podman run --name caddy docker.io/library/caddy:2.7.6-alpine
```

Currently, however, not starting the process is desired, because the default
Caddy configuration doesn't emit logs, and this leaves you without any data for
Vector to process. You can rectify this issue by modifying the default
configuration first, and only then starting the main `caddy` process inside the
container.

Create a new file named `Caddyfile` and paste the following contents, to ensure
that logs will be generated:

```text
[label Caddyfile]
:80 {
  root * /usr/share/caddy
  file_server
  try_files {path} /index.html
[highlight]
  log {
    output net localhost:9000 {
      dial_timeout 5s
      soft_start
    }
  }
[/highlight]
}
```

The `log` directive instructs Caddy to start emitting logs over a network socket
listening for TCP connections at `localhost:9000` inside the pod. This network
socket doesn't exist yet, but it will be created by the Vector container that
you'll set up next.

Copy the updated `Caddyfile` to the Caddy container by issuing:

```command
podman cp Caddyfile caddy:/etc/caddy/Caddyfile
```

Note how you are referring to the container by the name that you specified
earlier (`caddy`). This is a lot easier than writing:

```command
podman cp Caddyfile 7307f130b295:/etc/caddy/Caddyfile
```

You're almost ready to start the main `caddy` process. But before that, let's
quickly customize the homepage that it's going to serve, just so it's easier to
display its contents in a terminal.

Create a new file named `index.html` and paste the following contents:

```html
[label index.html]
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Example</title>
</head>
<body>
<p>This is an example page.</p>
</body>
</html>
```

Then copy the `index.html` file to the container by issuing:

```command
podman cp index.html caddy:/usr/share/caddy/index.html
```

Finally, start the Caddy container:

```command
podman start caddy
```

Once again, you're using the name you specified earlier (`caddy`) to identify
the container. This is why choosing clear and descriptive names is so important.

Confirm that the container is running by typing:

```command
podman ps -f name=caddy
```

A similar output should appear:

```text
[output]
CONTAINER ID  IMAGE                                 COMMAND               CREATED        STATUS                 PORTS                 NAMES
7307f130b295  docker.io/library/caddy:2.7.6-alpine  caddy run --confi...  7 minutes ago  Up About a minute ago  0.0.0.0:8080->80/tcp  caddy
```

Try accessing the server again:

```command
curl localhost:8080
```

This time, the expected output appears:

```text
[output]
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Example</title>
</head>
<body>
<p>This is an example page.</p>
</body>
</html>
```

Good, Caddy works and the `example` pod is capable of receiving HTTP requests on
port `8080` and forwarding them for processing to the Caddy container (on port
`80`).

You can also access your server from a web browser. Type in `localhost:8080` and
a similar web page should appear:

![14.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/3a474a5e-4274-4869-9713-9a65ce04a200/md2x =962x266)

Earlier, we mentioned that you cannot expose additional ports for a specific
container after providing the initial pod definition. Let's confirm this.

Create another pod:

```command
podman pod create dummy-pod
```

Now, try adding a new Caddy container to that pod, attempting to publish port
`80` of the container to port `8081` on the host:

```command
podman create --pod dummy-pod --publish 8081:80 docker.io/library/caddy:2.7.6-alpine
```

You get an error:

```text
Error: invalid config provided: published or exposed ports must be defined when the pod is created: network cannot be configured when it is shared with a pod
```

With this clarified, you're now ready to start setting up the Vector container.

Sign into your [Better Stack](https://telemetry.betterstack.com) account and create a
new data source:

![15.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/904690b8-5661-47b9-61df-78fc9ad9bb00/public =962x168)

In the presented form, specify **Podman tutorial** as the name and **Vector** as
the platform, then click **Create source**:

![16.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/fb86a998-5aac-4850-b07a-b8e4f0727600/lg1x =943x662)

If all goes well, the new source will be created successfully. Copy the token
presented under the **Source token** field. We'll refer to this token as
`<your_source_token>` and use it for configuring Vector to send logs to Better
Stack.

![17.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/1a0e4ef1-eb32-4b02-ac3f-50b57709b500/md2x =944x777)

Now create a new file named `vector.yaml` and paste the following contents:

```yaml
[label vector.yaml]
sources:
  caddy:
    type: socket
    address: 0.0.0.0:9000
    mode: tcp

sinks:
  better_stack:
    type: "http"
    method: "post"
    inputs: ["caddy"]
    uri: "https://in.logs.betterstack.com/"
    encoding:
      codec: "json"
    auth:
      strategy: "bearer"
[highlight]
      token: "<your_source_token>"
[/highlight]
```

This file will instruct the main process running inside the Vector container to
create a new network socket listening for TCP connections on port `9000`. Caddy
will connect to this socket to emit its logs. Furthermore, this configuration
will tell Vector to forward all collected logs over to Better Stack via HTTP.

Create a new container running the
[official Vector image](https://hub.docker.com/r/timberio/vector) and add it to
the `example` pod:

```command
podman create --pod example --name vector docker.io/timberio/vector:0.35.0-alpine
```

Copy the configuration file to the container:

```command
podman cp vector.yaml vector:/etc/vector/vector.yaml
```

Finally, start the container:

```command
podman start vector
```

Verify that all containers inside the pod are running by typing:

```command
podman ps --pod
```

You should see a similar output:

```text
CONTAINER ID  IMAGE                                    COMMAND               CREATED         STATUS             PORTS                 NAMES               POD ID        PODNAME
5827494c3cce  localhost/podman-pause:4.3.1-0                                 12 minutes ago  Up 3 minutes ago   0.0.0.0:8080->80/tcp  bf97c02c7c07-infra  bf97c02c7c07  example
7307f130b295  docker.io/library/caddy:2.7.6-alpine     caddy run --confi...  12 minutes ago  Up 3 minutes ago   0.0.0.0:8080->80/tcp  caddy               bf97c02c7c07  example
cd2daa5962e1  docker.io/timberio/vector:0.35.0-alpine                        33 seconds ago  Up 21 seconds ago  0.0.0.0:8080->80/tcp  vector              bf97c02c7c07  example
```

Now navigate back to your browser, and refresh the web page at `localhost:8080`
a couple of times, or issue a couple of `curl localhost:8080` commands from the
terminal.

```command
curl localhost:8080/[1-10]
```

In Better Stack, navigate to **Live tail**:

![Better Stack Live Tail link](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/36ebe019-6bde-4a8b-5e0d-de2d1ab4f400/lg2x =210x121)

You should see some logs collected from the Caddy container:

![Live tail output](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/680e05a5-eea5-4c72-e859-4c4e1c9b9900/lg2x =955x758)

Your setup works. The Caddy and Vector containers run in the same network
namespace, so they can communicate over the TCP socket that `vector`
established.

To confirm that the network namespace is the same, run:

```command
lsns -t net -p $(pgrep caddy)
```

```text
[output]
        NS TYPE NPROCS    PID USER     NETNSID NSFS COMMAND
4026532340 net       5 166215 marin unassigned      rootlessport
```

```command
lsns -t net -p $(pgrep vector)
```

```text
[output]
        NS TYPE NPROCS    PID USER     NETNSID NSFS COMMAND
4026532340 net       5 166215 marin unassigned      rootlessport
```

Both processes run in the network namespace with file descriptor `4026532340`.
The `rootlessport` command is a port forwarder which, when running Podman in
rootless mode, facilitates the forwarding of traffic from port `80` on the host
machine to port `8080` within the network namespace held by the pod.

With all of this out of the way, let's go ahead and explore how Podman can be
used for generating manifests and deploying them to a Kubernetes cluster, and
how existing Kubernetes manifests can be deployed into a local Podman
installation.

Make sure to leave your `example` pod running, as you're going to need it in the
next section.

## Integrating with Kubernetes

As I mentioned earlier, Podman doesn't ship with a tool such as Docker Swarm for
managing container orchestration. In a more sophisticated deployment scenario,
where high availability, scalability, and fault tolerance are required and
multiple hosts need to be involved, Podman users can leverage an orchestrator
such as Kubernetes to handle the complexity of managing their workloads.

Podman aims to ease the transition to and from Kubernetes by exposing commands
for converting existing workloads to YAML files (manifests) that Kubernetes can
understand. Furthermore, users can import existing Kubernetes manifests into
Podman, and Podman can parse and run these workloads locally.

If you're not familiar with what a Kubernetes manifest is, it's a file that
describes the desired state of your Kubernetes cluster. It includes information
about the pods, volumes, and other resources that have to be created and managed
by Kubernetes.

Before proceeding with this example, you have to install
[minikube](https://minikube.sigs.k8s.io/docs/) to be able to experiment with
Kubernetes locally. If you don't know what Minikube is, it is a tool allowing
you to run a single-node Kubernetes cluster on your local machine.

Follow the
[official Minikube installation instructions](https://minikube.sigs.k8s.io/docs/start/)
and run:

```command
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
```

This will download a binary file named `minikube-linux-amd64` into your current
directory. Use the following command to move this file to one of the directories
specified in your `$PATH`:

```command
sudo install minikube-linux-amd64 /usr/local/bin/minikube
```

This will enable you to run the `minikube` command from anywhere in your
terminal.

Since the `install` command doesn't move, but only copies the
`minikube-linux-amd64` file to the `/usr/local/bin` directory, you can go ahead
and remove the redundant copy by issuing:

```command
rm minikube-linux-amd64
```

To confirm that `minikube` has been installed successfully, run:

```command
minikube version
```

You should see a similar output:

```text
[output]
minikube version: v1.32.0
commit: 8220a6eb95f0a4d75f7f2d7b14cef975f050512d
```

Since the [Podman driver](https://minikube.sigs.k8s.io/docs/drivers/podman/) for
Minikube is still in experimental stage at the time of this writing, and this
causes some networking and DNS resolution issues inside Minikube depending on
the specific underlying setup, for a stable Minikube experience under Linux, you
still have to use Docker.

If you don't have Docker installed, you can generally follow the
[official Docker installation instructions](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository).

The examples that follow assume that Docker Engine is already installed and
running on your system, which you can verify by issuing:

```command
docker --version
```

You should see a similar output:

```text
[output]
Docker version 24.0.7, build afdd53b
```

You also need to make sure that your current user is added to the `docker`
group, so `sudo` isn't required for running commands against the Docker daemon:

```command
sudo usermod -aG docker $USER && newgrp docker
```

Otherwise, Minikube will fail with a similar error:

```text
[output]
👎  Unable to pick a default driver. Here is what was considered, in preference order:
    ▪ docker: Not healthy: "docker version --format {{.Server.Os}}-{{.Server.Version}}:{{.Server.Platform.Name}}" exit status 1: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/versio
n": dial unix /var/run/docker.sock: connect: permission denied
```

With all of these out of the way, go ahead and start Minikube:

```command
minikube start
```

You should see a similar output:

```text
[output]
😄  minikube v1.32.0 on Ubuntu 23.10 (kvm/amd64)
✨  Automatically selected the docker driver. Other choices: none, ssh
📌  Using Docker driver with root privileges
👍  Starting control plane node minikube in cluster minikube
🚜  Pulling base image ...
💾  Downloading Kubernetes v1.28.3 preload ...
    > preloaded-images-k8s-v18-v1...:  403.35 MiB / 403.35 MiB  100.00% 36.90 M
    > gcr.io/k8s-minikube/kicbase...:  453.88 MiB / 453.90 MiB  100.00% 36.32 M
🔥  Creating docker container (CPUs=2, Memory=2200MB) ...
🐳  Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
    ▪ Generating certificates and keys ...
    ▪ Booting up control plane ...
    ▪ Configuring RBAC rules ...
🔗  Configuring bridge CNI (Container Networking Interface) ...
🔎  Verifying Kubernetes components...
    ▪ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟  Enabled addons: storage-provisioner, default-storageclass
💡  kubectl not found. If you need it, try: 'minikube kubectl -- get pods -A'
🏄  Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
```

With Minikube running, you can proceed to generating Kubernetes manifests from
your Podman resources.

Verify that the `example` pod that you created earlier, along with all of its
containers, are still running:

```command
podman pod ls
```

```text
[output]
POD ID        NAME        STATUS      CREATED        INFRA ID      # OF CONTAINERS
bf97c02c7c07  example     Running     7 minutes ago  5827494c3cce  3
```

```command
podman container ps
```

```text
[output]
CONTAINER ID  IMAGE                                    COMMAND               CREATED        STATUS            PORTS                 NAMES
5827494c3cce  localhost/podman-pause:4.3.1-0                                 8 minutes ago  Up 8 minutes ago  0.0.0.0:8080->80/tcp  bf97c02c7c07-infra
7307f130b295  docker.io/library/caddy:2.7.6-alpine     caddy run --confi...  8 minutes ago  Up 8 minutes ago  0.0.0.0:8080->80/tcp  caddy
cd2daa5962e1  docker.io/timberio/vector:0.35.0-alpine                        8 minutes ago  Up 7 minutes ago  0.0.0.0:8080->80/tcp  vector
```

Podman can easily build a Kubernetes manifest from a running pod through the
`podman kube generate` command. It expects you to provide the following
parameters:

```text
[output]
podman kube generate <pod_name> --service -f <output_file>
```

To create the necessary manifest corresponding to your `example` pod, type:

```command
podman kube generate example --service -f example.yaml
```

In this process, you may observe the following warning, but since these
particular annotations don't carry any significant meaning, you can safely
disregard the message:

```text
[output]
WARN[0000] Truncation Annotation: "5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140bf" to "5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140b": Kubernetes only allows 63 characters
WARN[0000] Truncation Annotation: "5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140bf" to "5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140b": Kubernetes only allows 63 characters
```

`5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140bf` in this case
is the SHA-256 ID of the infrastructure container associated with the pod, which
is used for populating the `io.kubernetes.cri-o.SandboxID/caddy` and
`io.kubernetes.cri-o.SandboxID/vector` annotations inside the generated manifest
file. These annotations play no significant role for the deployment of this pod
to Kubernetes.

An `example.yaml` file should now appear in your current folder:

```command
ls -l example.yaml
```

```text
[output]
-rw-r--r-- 1 marin marin 2270 Jan 19 11:21 example.yaml
```

Let's examine its contents:

```yaml
[label example.yaml]
# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-4.3.1
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: "2024-01-19T11:21:33Z"
  labels:
    app: example
  name: example
spec:
  ports:
    - name: "80"
      nodePort: 30381
      port: 80
      targetPort: 80
  selector:
    app: example
  type: NodePort
---
apiVersion: v1
kind: Pod
metadata:
  annotations:
    io.kubernetes.cri-o.ContainerType/caddy: container
    io.kubernetes.cri-o.ContainerType/vector: container
    io.kubernetes.cri-o.SandboxID/caddy: 5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140bf
    io.kubernetes.cri-o.SandboxID/vector: 5827494c3cce19080da3e0804596c4f46c71c342429d8171bfa45f4188b140bf
    io.kubernetes.cri-o.TTY/caddy: "false"
    io.kubernetes.cri-o.TTY/vector: "false"
    io.podman.annotations.autoremove/caddy: "FALSE"
    io.podman.annotations.autoremove/vector: "FALSE"
    io.podman.annotations.init/caddy: "FALSE"
    io.podman.annotations.init/vector: "FALSE"
    io.podman.annotations.privileged/caddy: "FALSE"
    io.podman.annotations.privileged/vector: "FALSE"
    io.podman.annotations.publish-all/caddy: "FALSE"
    io.podman.annotations.publish-all/vector: "FALSE"
  creationTimestamp: "2024-01-19T11:21:33Z"
  labels:
    app: example
  name: example
spec:
  automountServiceAccountToken: false
  containers:
    - image: docker.io/library/caddy:2.7.6-alpine
      name: caddy
      ports:
        - containerPort: 80
          hostPort: 8080
      resources: {}
      securityContext:
      capabilities:
        drop:
          - CAP_MKNOD
          - CAP_NET_RAW
          - CAP_AUDIT_WRITE
    - image: docker.io/timberio/vector:0.35.0-alpine
      name: vector
      resources: {}
      securityContext:
        capabilities:
          drop:
            - CAP_MKNOD
            - CAP_NET_RAW
            - CAP_AUDIT_WRITE
  enableServiceLinks: false
  hostname: example
  restartPolicy: Never
status: {}
```

You can now run the following command to deploy this manifest to your Kubernetes
cluster:

```command
minikube kubectl -- create -f example.yaml
```

This results in a similar output:

```text
[output]
    > kubectl.sha256:  64 B / 64 B [-------------------------] 100.00% ? p/s 0s
    > kubectl:  47.56 MiB / 47.56 MiB [------------] 100.00% 2.42 GiB p/s 200ms
service/example created
pod/example created
```

Wait a minute or two, then type:

```command
minikube kubectl -- get all
```

You should see a similar output:

```text
[output]
NAME          READY   STATUS    RESTARTS   AGE
pod/example   2/2     Running   0          7m11s

NAME                 TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
service/example      NodePort    10.110.196.168   <none>        80:30381/TCP   7m11s
service/kubernetes   ClusterIP   10.96.0.1        <none>        443/TCP        11m
```

This indicates that the pod is up and running inside your local Kubernetes
cluster.

From the output, it appears that the Pod is ready to accept incoming HTTP
requests on port `80` through the corresponding `NodePort` service. In this
case, the `NodePort` service basically maps port `30381` of the Kubernetes node
that the pod is running on to port `80` in the pod.

However, if you type in:

```command
curl localhost:80
```

You'll notice that the web server is unreachable:

```text
[output]
curl: (7) Failed to connect to localhost port 80 after 0 ms: Couldn't connect to server
```

That's because the `minikube` network is isolated from your host network. You
can run the following command to determine the URL that you can connect to:

```command
minikube service list
```

This will output a similar table:

```text
[output]
|-------------|------------|--------------|---------------------------|
|  NAMESPACE  |    NAME    | TARGET PORT  |            URL            |
|-------------|------------|--------------|---------------------------|
| default     | example    | 80/80        | http://192.168.49.2:30381 |
| default     | kubernetes | No node port |                           |
| kube-system | kube-dns   | No node port |                           |
|-------------|------------|--------------|---------------------------|
```

The address listed in the `URL` column is the one enabling access to your web
server.

Try again, and open `http://192.168.49.2:30381` in a browser or type:

```command
curl http://192.168.49.2:30381
```

You'll see the familiar "Caddy, works!" page:

!["Caddy works!" page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/4056ae9c-6dba-4a9e-10fc-db762064c500/public =973x341)

Your pod is now successfully running on Kubernetes. The changes that you did
before through `podman cp` are, of course, missing from the deployed images, so
Caddy defaults to displaying the "Caddy, works!" page, but essentially all it
took to deploy the application to Kubernetes was a single command.

You can remove the pod from Kubernetes by typing:

```command
podman kube down example.yaml
```

This produces a similar output:

```text
[output]
Pods stopped:
98e78483cfd2258fa5d82fb77d113b9cbdd39adc33712ea448b4de15800bb4ce
Pods removed:
98e78483cfd2258fa5d82fb77d113b9cbdd39adc33712ea448b4de15800bb4ce
```

As you can see, with only a few commands, you were able to generate a manifest
for deploying your application on Kubernetes. Then, you took an existing
Kubernetes manifest and ran it locally with Podman. This demonstrates the power
and flexibility that Podman can provide for orchestrating your containerized
workloads.

## Exploring Podman Desktop

Even though using the CLI is a common way to interact with Podman, users who
prefer a graphical interface have the additional option of using
[Podman Desktop](https://podman-desktop.io/), an open-source tool that provides
a user-friendly GUI for managing containers and images and interacting with
Kubernetes manifests.

Podman Desktop aims to abstract away the low level details and let users focus
more on application development.

The usual way to install Podman Desktop is through its corresponding
[Flatpak](https://flatpak.org/) bundle. If you don't happen to have `flatpak`
installed on your system, you can install it by running:

```command
sudo apt install flatpak
```

Then add the `flathub` repository, as follows:

```command
flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
```

You may have to restart your session for all changes to take effect. When you're
done, you can run the following command to install Podman Desktop:

```command
flatpak install --user flathub io.podman_desktop.PodmanDesktop
```

Finally, to start Podman Desktop, run:

```command
flatpak run io.podman_desktop.PodmanDesktop
```

Soon after, the Podman Desktop GUI will appear:

![Podman Desktop main page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/ab404b92-c9bd-47aa-26be-ca3cbdb47700/public =967x645)

Let's recreate the pod from our previous examples by issuing the following
commands in the terminal:

```command
podman pod create --name example --publish 8080:80
```

```command
podman create --pod example --name caddy docker.io/library/caddy:2.7.6-alpine
```

```command
podman create --pod example --name vector docker.io/timberio/vector:0.35.0-alpine
```

Then, in Podman Desktop, navigate to **Pods**:

![Podman Desktop main page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/24e60b7a-8a7d-4e53-8e1b-473054635200/lg1x =976x649)

You will see the `example` pod listed:

![Podman Desktop pods page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/0d681d83-4bf3-4c19-a8dc-58553a438b00/md2x =973x651)

Instead of having to type `podman kube generate` to create a Kubernetes manifest
from this pod, you can use the **Generate Kube** action:

![Podman Desktop generate kube action](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/7fd89466-2ba2-4feb-3523-6b4b04b7fb00/lg1x =975x649)

A manifest appears, containing the same content that you would otherwise get by
running `podman kube generate example -f example.yaml`.

![Podman Desktop generated manifest](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/40502475-fe00-4221-9f59-321902ec2600/md2x =975x649)

You may have noticed though that a `Service` definition is missing from that
manifest. Earlier, you requested it explicitly by passing the `--service` flag
to `podman kube generate`. At first sight, it may appear that Podman Desktop
doesn't allow you to define a `Service` easily. However, this isn't the case.

Go back to the **Pods** screen and select the **Deploy to Kubernetes** action:

![Deploy to Kubernetes action](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/7355fe26-551e-4ee9-d8b3-7c6b7d006300/md1x =974x648)

The same YAML definition appears, but there is also an additional checkbox
allowing you to define a `Service`:

![Podman Desktop generated manifest](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/6f23182f-2c55-4890-b17c-f6d14846e600/public =972x651)

Scroll down a little bit, and you will see `minikube` listed as the Kubernetes
context. This corresponds to the `minikibe` cluster you created earlier:

![Deploy to Kubernetes action](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/06a0d4db-64bb-4960-93c3-0bcb80077800/md1x =975x650)

Click **Deploy** and after a few moments the pod will get deployed to your local
`minikube` cluster:

![Deploy to Kubernetes action](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/63daef64-5e57-4b51-99f6-1b81f89b7f00/md1x =974x649)

Go back to the terminal and issue:

```command
minikube service list
```

This outputs:

```text
[output]
|-------------|--------------|--------------|-----|
|  NAMESPACE  |     NAME     | TARGET PORT  | URL |
|-------------|--------------|--------------|-----|
| default     | example-8080 | No node port |     |
| default     | kubernetes   | No node port |     |
| kube-system | kube-dns     | No node port |     |
|-------------|--------------|--------------|-----|
```

Unlike before, even though a service was created, there is no node port
available for connecting to Caddy. That's because Podman Desktop created a
service of type `ClusterIP` instead of `NodePort`.

To verify this, issue:

```command
minikube kubectl -- get all
```

You'll see that the `example-8080` service created by Podman Desktop has a type
of `ClusterIP`:

```text
[output]
NAME          READY   STATUS    RESTARTS   AGE
pod/example   2/2     Running   0          4m25s

NAME                   TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)    AGE
service/example-8080   ClusterIP   10.105.82.9   <none>        8080/TCP   4m25s
service/kubernetes     ClusterIP   10.96.0.1     <none>        443/TCP    6m21s
```

One possible way to address this problem in order to access Caddy is by patching
the service to change its type:

```command
 minikube kubectl -- patch svc example-8080 --type='json' -p '[{"op":"replace","path":"/spec/type","value":"NodePort"}]
```

```text
[output]
service/example-8080 patched
```

You can now re-run:

```command
minikube service list
```

This time, a URL appears allowing you to access Caddy:

```text
[output]
|-------------|--------------|-------------------|---------------------------|
|  NAMESPACE  |     NAME     |    TARGET PORT    |            URL            |
|-------------|--------------|-------------------|---------------------------|
| default     | example-8080 | example-8080/8080 | http://192.168.49.2:30381 |
| default     | kubernetes   | No node port      |                           |
| kube-system | kube-dns     | No node port      |                           |
|-------------|--------------|-------------------|---------------------------|
```

Open the listed URL in a browser and you'll see a familiar page:

![Caddy works page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/5f8dadb6-0d79-4ce2-6cac-0f82c137a500/md1x =962x339)

Everything appears to work correctly!

Next, let's explore how importing an existing Kubernetes manifest works with
Podman Desktop. Before that, however, let's remove all pods created so far in
order to start in a clean state.

Open Podman Desktop and navigate to the Pods page. You will see both the Podman
and the Kubernetes `example` pods appearing in the list:

![example pods](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/88e756b7-1731-4710-42a9-b2e47e053f00/orig =976x648)

Click the **Delete** buttons next to each pod in order to remove them from your
system:

![delete pods](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/b285ff5f-b7ec-41a2-fb44-26ccda246200/lg2x =975x649)

When you're done, you should see an empty list of pods:

![Podman Desktop pods page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/d4b7387d-94a5-4901-7f81-fbbba90a0500/public =972x650)

Click on the **Play Kubernetes YAML** button at the top right of the Pods
screen:

![Play Kubernetes YAML button](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c6f0ca40-1267-4418-1b69-ae6a67914c00/orig =971x651)

Press that button, and a form will appear prompting you to specify a `*.yaml`
file to execute:

![Play Kubernetes YAML form](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/33a9c0ff-03b5-4e30-ee75-02acf85e2800/md1x =969x649)

Select the `example.yaml` file that you created earlier and click **Play**:

![Play Kubernetes YAML form populated](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/ae82c8ce-df91-4478-a15b-17905dc62b00/lg2x =970x649)

A message appears prompting you to wait while Podman orchestrates your
containers:

![Play Kubernetes YAML loading](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/6742a553-2446-43c9-1e05-bbe6c84ad100/md1x =971x647)

After a moment, the process completes and Podman Desktop displays a JSON
document indicating that the pod was started:

![Play Kubernetes YAML done](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/c5cf9d19-96c0-45e0-5d8c-b37c07422200/lg1x =970x647)

You can click the **Done** button, after which you'll see the newly created
`example` pod in the list of pods:

![Podman desktop pods page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/03cfa5d5-27c3-4c17-f029-cecb2a067c00/md2x =968x650)

Effectively, this entire process performs the same actions as the
`podman kube play example.yaml` command you used earlier.

Open `localhost:8080` in a browser, and it will take you to the familiar Caddy
homepage:

![Caddy test page](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/ce3152e2-f6f4-4042-65e4-3f0a59b6e800/lg2x =968x303)

To remove the pod and all of its attached containers in a way similar to
`podman kube down`, just navigate back to the Pods page and click **Delete
Pod**:

![Podman Desktop delete pod](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/8ee8f810-37ce-475f-c740-9ef2f67f6000/lg1x =973x647)

A loader icon appears and soon after the pod is gone:

![Podman Desktop delete pod](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/5244bdf4-e488-403f-e88b-f8c7d2f52600/md1x =973x650)

As you can see, Podman Desktop provides a convenient interface for managing your
pods, making it easy to create, view, and delete them with just a few clicks. It
also simplifies the process of working with Kubernetes and allows you to quickly
perform actions like creating pods, accessing their public-facing services, and
removing them when they are no longer needed. With Podman Desktop, you can
effectively manage your containerized applications without the need for complex
command-line instructions.

## Final thoughts

The ability of Podman to integrate with Kubernetes presents a promising and
flexible solution for container orchestration in modern IT environments. You can
take advantage of these capabilities to seamlessly manage and deploy your
containers across development, staging, and production environments.

For example, you can prototype your applications locally using Podman before
eventually deploying them to a shared Kubernetes cluster for testing. You can
also import externally provided Kubernetes manifests into your local Podman
environments in order to explore and validate the behavior of applications
without the need to run full-fledged Kubernetes clusters.

The options are endless, and both Podman CLI and Podman Desktop provide the
necessary tools and flexibility for you to efficiently work with pods in various
scenarios. To explore Podman further, consider visiting the
[official Podman website](https://podman.io/), exploring its
[documentation](https://podman.io/docs), and joining its growing
[community](https://podman.io/community).

Thanks for reading!