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
1ab49c19c53ebca95c787b482aeda86d1d681f58cdf19278c476bcaf37d96de1ZshGoes 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
/ #ZshAnd 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: bytesZshWorks 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
46176f9d54e2a0b0cce376daf288c51f065fb8eb190a53d107f992ecc02dd6bdZshThen 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...ZshTechnically, 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
- tagsYAMLThen 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-$RUNNERYAMLThen 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_NAMEYAMLNow, 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...ZshAnd 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.
Leave a Reply