Podman aventures


Recently, I’ve started playing around a bit more with Podman. If you’re not familiar with it, it’s a container engine in the same vein as Docker, only it can run completely rootless and uses OCI containers. Their licensing is also a bit more generous than Docker’s, for instance, Podman desktop doesn’t require a license for commercial use, while Docker Desktop does.

Before we being, so far, I tested Podman on Fedora Linux, MacOS, and through GitLab CI/CD; but I know there is much more to it, maybe for another blog post about it.

The first thing I did was, of course, pulling an image. And as you would expect, you get… an image.

podman pull docker.io/alpine:latest
Trying to pull docker.io/library/alpine:latest...
Getting image source signatures
Copying blob sha256:d8ad8cd72600f46cc068e16c39046ebc76526e41051f43a8c249884b200936c0
Copying config sha256:1ab49c19c53ebca95c787b482aeda86d1d681f58cdf19278c476bcaf37d96de1
Writing manifest to image destination
1ab49c19c53ebca95c787b482aeda86d1d681f58cdf19278c476bcaf37d96de1
Zsh

Goes pretty fast in general; now we can also launch the container:

podman run -it --rm docker.io/alpine:latest
/ # uname -a
Linux c0d5eb4555e4 6.17.7-300.fc43.aarch64 #1 SMP PREEMPT_DYNAMIC Sun Nov  2 15:33:04 UTC 2025 aarch64 Linux
/ #
Zsh

And sure enough, it works. How about running a web server, this should be more complex, right?

podman run --rm -p 8080:80 docker.io/nginx:latest
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob sha256:2668e34349761086c8c3ceb1f269a65e54326d52c5e222c7f686c48825a10cd3
Copying blob sha256:3b66ab8c894cad95899b704e688938517870850391d1349c862c2b09214acb86
Copying blob sha256:4a89256e588a59c19954ebc8a66aa7d464370096f4d7e5b52efde9cfd5d70a2d
Copying blob sha256:c813174c999b01107d86097d711f5a38d5dfc40c51192f44b93859528fbf6f66
Copying blob sha256:901e94f777d19c29bda7ca9a1d6a4761a5a2e0f7ce1ace6caeb60e3268335e5f
Copying blob sha256:e88d7844c33ddce38435479107a2bc900d129647ddc6a182bdd7b57f4796f6f8
Copying blob sha256:f2c05cdfb14987a928321a4e4fd9386113f00e4f7acaf43aa08716680a704f46
Copying config sha256:0000f06a0cafed8b95a6dabaec83c50aa674280ffb0e9ee3ff87521518580bcc
Writing manifest to image destination
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2026/03/05 20:31:14 [notice] 1#1: using the "epoll" event method
2026/03/05 20:31:14 [notice] 1#1: nginx/1.29.5
2026/03/05 20:31:14 [notice] 1#1: built by gcc 14.2.0 (Debian 14.2.0-19)
2026/03/05 20:31:14 [notice] 1#1: OS: Linux 6.17.7-300.fc43.aarch64
2026/03/05 20:31:14 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
2026/03/05 20:31:14 [notice] 1#1: start worker processes
2026/03/05 20:31:14 [notice] 1#1: start worker process 24
2026/03/05 20:31:14 [notice] 1#1: start worker process 25
2026/03/05 20:31:14 [notice] 1#1: start worker process 26
2026/03/05 20:31:14 [notice] 1#1: start worker process 27
2026/03/05 20:31:14 [notice] 1#1: start worker process 28
2026/03/05 20:31:14 [notice] 1#1: start worker process 29

# In other terminal
curl -I localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.29.5
Date: Thu, 05 Mar 2026 20:31:40 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Wed, 04 Feb 2026 15:12:20 GMT
Connection: keep-alive
ETag: "698361d4-267"
Accept-Ranges: bytes
Zsh

Works flawlessly as well. Testing on even more advanced tooling (which I doubt I can showcase on this blog due it being work stuff), Podman also does the job. As a side note, it can also build existing solutions from a Dockerfile, so pretty cool.

For instance, building the Crystalline Shard Resonite bot:

podman build -t local.internal/ng-test:latest .
[1/2] STEP 1/4: FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
Trying to pull mcr.microsoft.com/dotnet/sdk:9.0...
Getting image source signatures

# Very long build output...

--> 0ea8e8dfaf91
[2/2] STEP 5/5: ENTRYPOINT ["dotnet", "ResoniteDiscordMetrics.dll"]
[2/2] COMMIT local.internal/ng-test:latest
--> 46176f9d54e2
Successfully tagged local.internal/ng-test:latest
46176f9d54e2a0b0cce376daf288c51f065fb8eb190a53d107f992ecc02dd6bd
Zsh

Then running it:

podman run --rm local.internal/ng-test:latest
crit: Discord.WebSocket.DiscordSocketClient[0]
      Startup error: DISCORD_TOKEN env variable not provided.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
Zsh

Technically, Podman can also use the Containerfile format, which is very similar to the Dockerfile one (at least, to my not yet educated eyes).

Now, my favourite part, let’s automate some builds. Taking the aforementioned Crystalline Shard Resonite bot, we’ll make a multi-arch build within GitLab that then will be pushed to the internal container registry. For this, we will be using Buildah.

For the curious, $IMAGE_NAME is defined by $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME in the following code.

Starting with a small template to set some basics like the image and auth:

.oci-base:
  image: quay.io/buildah/stable
  stage: container
  before_script:
    - echo "$CI_REGISTRY_PASSWORD" | buildah login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY
  needs:
    - build
  only:
    - branches
    - tags
YAML

Then creating a build step with a Matrix as to not waste lines. Also small aparte, it seems that .NET really doesn’t likes building multi-arch images in one go, so that’s why we’re going with two at first:

oci:build:
  extends: .oci-base

  parallel:
    matrix:
      - RUNNER: ["amd64", "arm64"]

  tags:
    - $RUNNER

  script:
    - buildah build --jobs 1 --platform=linux/$RUNNER -t $IMAGE_NAME-$RUNNER .
    - buildah push $IMAGE_NAME-$RUNNER
YAML

Then the final magic sauce: combining the images. I was unaware this was a feature at first, but turns out Podman can combine two images in one so it becomes multi-arch:

oci:build:combine:
  extends: .oci-base
  needs:
    - oci:build

  script:
    - buildah manifest create $IMAGE_NAME
    - buildah manifest add $IMAGE_NAME docker://$IMAGE_NAME-arm64
    - buildah manifest add $IMAGE_NAME docker://$IMAGE_NAME-amd64
    - buildah manifest push --all $IMAGE_NAME
YAML

Now, the image is pushed and ready to be used:

podman run --rm d.j4.lc/crystalline-shard/oss/resonite-discord-sessions:main
Trying to pull d.j4.lc/crystalline-shard/oss/resonite-discord-sessions:main...
Getting image source signatures
Copying blob sha256:739c3d55addb818e52b6004a27658992b10fa750bcad2961acd74efcc79ea48f
Copying blob sha256:8da7bbdcb33ac3b6efd70dbff95fe85bb96e49008404c990e1ccefdd109d4c2a
Copying blob sha256:6fbfe6f2d7fae894d624657623381dada3175970542edded91632ae63c421f9e
Copying blob sha256:d362a8134437649022ecb750cc549fb675f727755d430ddd932db1e8f7fcb356
Copying blob sha256:7f7c9eb9e6965b2fa2b33862a4a3d9c4a844ee217a13f9ec3325d0eab126f8d1
Copying blob sha256:642707d1dbb9c473608605ed51efd8c60c4b4f1a1810cf6c81f27a045fe185e9
Copying config sha256:f17dca5b98e16301296d5461c28f0f6105ae29fa5428cf1682f29dc89842b202
Writing manifest to image destination
crit: Discord.WebSocket.DiscordSocketClient[0]
      Startup error: DISCORD_TOKEN env variable not provided.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /app
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
Zsh

And that’s it, we achieved multi-arch builds with Podman!

There’s still much more I want to explore with Podman, notably the systemd integration which seems really useful. Stay tuned for more later.


Jae's Blog
Jae's Blog
@b@b.j4.lc

Jae’s blog, now federating properly!

123 posts
39 followers

Comments

Likes

Reposts

Leave a Reply

Your email address will not be published. Required fields are marked *

To respond on your own website, enter the URL of your response which should contain a link to this post’s permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post’s URL again. (Find out more about Webmentions.)