When Docker [1] was released it felt like a bang. Nothing it did was really revolutionary, the Linux features that are used to provide sandboxed environments were already in place for a long time. But what Docker provided was a dead-simple way to interact with these features and this level of user friendliness was not achieved until that moment. For me, Docker was a revelation as it simplified a multitude of tasks, be it application management on servers, preparation of demo systems or just usage of utilities. No longer was I confined to the world of packages provided by a given distribution, but could mix and match as needed and have a consistent interface, a common format to exchange resources as well as the infrastructure to do so.

That’s nice! So why switch to something else?

After the hype faded (it shifted to another layer → Kubernetes [2]) it became apparent that some design choices of Docker were not ideal. For one, the need to deploy a central system service and API in order to run containers was a bit “clumsy”, especially if you simply want to run containers locally on your box. If this daemon encounters a problem and is not available the user is not able to manage any of her containerized workloads. Adding to that, this daemon usually runs in a privileged context, so giving access to Docker basically meant giving out root to Docker users. Changes were made to Docker in order to better isolate containers from each other with the help of user namespaces [3] and to provide a rootless [4] mode, but these features are still in an experimental state.

Enter podman

podman [6] is a program developed by Red Hat and other volunteers. It provides an easy to use interface to run unprivileged containers on a system. It does not need a daemon or root-rights and is compatible with the docker command line options, so if you are familiar with Docker, switching to podman should be as easy as alias docker=podman. Besides podman there are several other tools like buildahskopeocri-o and many others, all being part of the Containers [7] project initiated by Red Hat.

The use case at hand

B1 Systems is using LaTeX in order to build all the different types of documentation we are using internally and externally, for example documentation of projects realized for customers or material for our training courses. Many colleagues take part in creating and improving our documentation and training materials and may not have LaTeX before starting their job at B1. LaTeX is a great technology, but if you are using many modules/complex themes and so on it’s a challenge to prepare an environment to build docs, especially if you don’t usually touch LaTeX. The second challenge is the freedom and flexibility inside of B1. Colleagues are free to use whatever system they like (as long as they get work done 😉), so naturally there are a multitude of distributions in use inside of B1. My distribution of choice is Archlinux, but the documentation team is using OpenSUSE Leap, others use Fedora/Debian/Ubuntu/Gentoo etc. I wanted to create a lowest common denominator, so I grabbed a list of packages from the documentation team and built an automation recipe to make building and using the construct as simple as possible.

Docker compatibility

On principle, switching to podman is rather easy. Instead of reworking the build recipe, one could just change occurences of docker with podman 🙃. podman build (or buildah bud) will read a Dockerfile (Containerfile) and create a container image out of it. The initial Dockerfile may look something like this:

FROM opensuse/leap:15.2
MAINTAINER Mattias Giese

ARG package_list=latex-packages.list

RUN useradd -m -s /bin/bash build
ADD files/ /home/build
RUN chown -R build /home/build

ENV HOME /home/build

ADD $package_list /
RUN zypper ar -cfp 90 packman ;\
      zypper -n --gpg-auto-import-keys ref ;\
      zypper -n up zypper ;\
      zypper -n in --no-recommends texlive ;\
      xargs -a $package_list zypper -n in --force-resolution --no-recommends ;\
      zypper clean --all ;\
      rm -rf /var/tmp/ /usr/share/texmf/doc /usr/share/doc

WORKDIR /home/build/svn
RUN chmod +x /

The files directory contains a few support files like texmf data used to build presentations.

This example features a grief I had with Docker regarding the usage with unprivileged contexts: First, I need to create the unprivileged user in the container image and setup its environment. After that I need to create and add a special entry point in order to fix file permissions during build:

set -u -e -o pipefail
chown $ID . &>/dev/null
usermod -u $ID build &>/dev/null
sudo -u build bash -c "$CMD"

ID needs to be supplied to the running container as environment variable. It will change the UID of the build user to the specified ID, mangle filesystem permissions inside the container and then run the specified commands (CMD environment variable) as the build user. All this stuff is necessary because we mount the Subversion/git checkout of the documentation repository into the container at /home/build/svn and want the user on the host to be able to access/write to the produced files after the build. For the use case at hand this is ridiculous. We start a container as root and drop privileges inside of the container, just to let the user on the host access her files afterwards. There had to be a better way.

Using buildah

Instead of using a Containerfile/Dockerfile I chose the scripting route with buildah [5]. Instead of providing the whole script we show off the most interesting bits:


mkdir -p "$ZYPP_CACHE_DIR"
CTR=$(buildah from --pull --volume "${ZYPP_CACHE_DIR}:/var/cache/zypp" "$UPSTREAM_IMAGE")

run() {
  buildah run "$CTR" -- $@

add() {
  buildah add "$CTR" $@

Here we set a few environment variables and prepare our package cache directory. Instead of caching the complete build steps (like docker build does), we only cache the zypper package cache. That way we always run package installation/update steps, but rely on the already existing packages/metadata which speeds up the build tremendously. The first important command is buildah from, which creates a container reference that we use for all the following build steps. --pull tells buildah to always update the referenced base image and with --volume we bind our package cache into the build context. The functions run() and add() are used as a shortcut in the rest of the script.

echo Building container
set +u
if [[ -n "$FLAVOR" && -f "${FLAVOR}-packages.list" ]]; then
set -u
echo Using package list $package_list

add files /build
add "$package_list" /
run zypper ar -cfp 90 "${LEAP_RELEASE}/" packman
# enable package caching for all repos (otherwise the bind-mount /var/cache/zypp does not make sense)
run zypper mr -a -k
run zypper --gpg-auto-import-keys ref
run zypper -n up zypper
run xargs -a $package_list zypper -n in --force-resolution --no-recommends
run rm -rf /var/tmp/ /usr/share/texmf/doc
buildah config \
  --env 'HOME=/build' \
  --workingdir '/build/svn' \
  --annotation "de.b1-systems.builduser=$(git config --get" \

buildah commit "$CTR" "$DEST_IMAGE"

This part of the script now determines the package list to use, adds files to the image and runs a set of commands. With buildah we aren’t forced to awkwardly combine commands with constructs like ;\ in order to reduce the number of image layers produced (in order to minimize the image size), as every command up until the commit of the image will create just one image layer. This example also shows how flexible the scripting approach is. Instead of externalizing special preparation scripts and using a Dockerfile-based build afterwards we are able to create one script to handle the preparation and image building as one recipe. As you might have noticed as well we don’t create an unprivileged user inside the image. Thanks to user namespaces [8] we are able to start a container as an unprivileged user, but be root inside it (if we enable the specific setting, see kernel.unprivileged_userns_clone sysctl).

Getting to the finish line

Now, after the container is built we want to use it in our workflow. For this I created a simple shell function for bash/zsh:

doc-build() {

  CMD="cd ${BASEDIR}; make $@"
  # [ omitted some preparation / command mangling ]
  podman run -t -i --net=none --userns keep-id --rm $DOCKER_OPTS -v $(pwd):${BASEDIR}:z $IMAGE bash -c "$CMD"

The parameter --userns keep-id is our key to success. With this parameter, we run commands as “root” inside the container, but files will be created with the UID of the user running the container (instead of the mapped UID from /etc/subuid [9]).

Using the construct

I want to rebuild the chapter/whole document when I save it in my editor of choice [10]. For this, I use the wonderful utility entr [11]:

echo chapters/container-rhel8.tex | entr zsh 'doc-build chapters/container-rhel8.tex'

Now, every time I save the document specified, entr will spawn a shell and run the shell function, thus starting the container and kicking off the build in a clean/pre-determined environment. Now, everyone may use their favorite distribution in peace and still be able to write/build documentation and get support from colleagues if things go sideways 🧘

I personally love podman and used it since the first public release and have since then removed docker from all my personal boxes that run containers. A nifty feature I really like is podman generate systemd [12], which generates a systemd Service for the container for seamless integration into the hosts service management.

Mattias Giese
Mattias Giese is working as a Linux consultant and trainer at B1 Systems. If he isn't involved in systems management and automation projects where he works with a plethora of different tools that he glues together to create efficient workflows, he likes to mess around with chic mechanical keyboards and adjust the configuration of his tools in order to achieve zen laziness.

This site is registered on as a development site. Switch to a production site key to remove this banner.