Skip to main content

Building with Buildah: Dockerfiles, command line, or scripts

You can use Dockerfiles to build your container images, but these files have limitations. How about building them yourself?
Image
building blocks

Image by Pixabay

I don’t know about your family, but my family is crazy about building with LEGOs. Last year for Christmas each of us got at least one LEGO set. The first discussion after the unwrapping is complete regards who gets to build first at the favored kitchen table.

Building container images using Buildah can be like building a LEGO kit, too. Yes, you can build them using a Dockerfile, and that’s pretty straightforward, but that’s like having someone build your LEGO set for you. However, if you’d like to build the container image step-by-step yourself from the command line (or by using a Bash script), you can do that instead. As I’ve fielded a few questions on this topic in the recent past, I thought I’d run through a quick example of how to build a container image based on a Dockerfile using Bash.

Building with a Dockerfile

First, let’s look at the Dockerfile. This example uses the same Dockerfile that’s used to build the quay.io/buildah/upstream:latest image. It’s located here on GitHub. This Dockerfile is used by Quay.io to automatically build a new container image every time something is merged into Buildah’s GitHub repository.

Let’s take a quick peek at the Dockerfile:

# git/Dockerfile
#
# Build a Buildah container image from the latest
# upstream version of Buildah on GitHub.
# https://github.com/containers/buildah
# This image can be used to create a secured container
# that runs safely with privileges within the container.
# The containers created by this image also come with a
# Buildah development environment in /root/buildah.
#
FROM fedora:latest
ENV GOPATH=/root/buildah

# Install the software required to build Buildah.
# Then create a directory and clone from the Buildah
# GitHub repository, make and install Buildah
# to the container.
# Finally remove the buildah directory and a few other packages
# that are needed for building but not running Buildah

RUN dnf -y install --enablerepo=updates-testing \
     make \
     golang \
     bats \
     btrfs-progs-devel \
     device-mapper-devel \
     glib2-devel \
     gpgme-devel \
     libassuan-devel \
     libseccomp-devel \
     git \
     bzip2 \
     go-md2man \
     runc \
     fuse-overlayfs \
     fuse3 \
     containers-common; \
     mkdir /root/buildah; \
     git clone https://github.com/containers/buildah /root/buildah/src/github.com/containers/buildah; \
     cd /root/buildah/src/github.com/containers/buildah; \
     make;\
     make install;\
     rm -rf /root/buildah/*; \
     dnf -y remove bats git golang go-md2man make; \
     dnf clean all;

# Adjust storage.conf to enable Fuse storage.
RUN sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' /etc/containers/storage.conf
RUN mkdir -p /var/lib/shared/overlay-images /var/lib/shared/overlay-layers; touch /var/lib/shared/overlay-images/images.lock; touch /var/lib/shared/overlay-layers/layers.lock

# Set up environment variables to note that this is
# not starting with usernamespace and default to 
# isolate the filesystem with chroot.
ENV _BUILDAH_STARTED_IN_USERNS="" BUILDAH_ISOLATION=chroot

This Dockerfile pulls the latest Fedora release, sets up the GOPATH, runs a number of RUN commands to install dependencies and ensure that configuration files are set up properly, and then finishes by setting up an environment variable.

To build this container with a Dockerfile, it’s a pretty simple command:

# buildah bud -t buildahupstream:latest .

and off it will go. But what fun is that?

Building on the command line

Now, let’s try building the container one piece at a time using the Buildah command line.

Building the FROM step equivalents

First, we need to do the FROM step and capture the resulting container name in the ctr variable:

# ctr=$(buildah from fedora)
Getting image source signatures
Copying blob d318c91bf2a8 done
Copying config f0858ad3fe done
Writing manifest to image destination
Storing signatures

Use this command to set the GOPATH:

# buildah config --env GOPATH=/root/buildah $ctr

Building the RUN command equivalents

Now for the RUN commands. Although long, and honestly not something I’d want to type in by hand, this example shows how quickly you can translate any Dockerfile command. It really is simple to translate from that format to Buildah commands with only a little Bash tweaking. They look like:

# buildah run $ctr /bin/sh -c 'dnf -y install --enablerepo=updates-testing \
     make \
     golang \
     bats \
     btrfs-progs-devel \
     device-mapper-devel \
     glib2-devel \
     gpgme-devel \
     libassuan-devel \
     libseccomp-devel \
     git \
     bzip2 \
     go-md2man \
     runc \
     fuse-overlayfs \
     fuse3 \
     containers-common; \
     mkdir -p /root/buildah; \
     git clone https://github.com/containers/buildah /root/buildah/src/github.com/containers/buildah; \
     cd /root/buildah/src/github.com/containers/buildah; \
     make;\
     make install;\
     rm -rf /root/buildah/*; \
     dnf -y remove bats git golang go-md2man make; \
     dnf clean all'  

Adding a status check

This next step wasn’t part of the Dockerfile and is not necessary, but it’s useful for checking the status of our container build. Let’s use the buildah mount command and capture the returned directory (which is the container’s root filesystem) in the mnt variable:

# mnt=$(buildah from $ctr)
# buildah run $ctr -- sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' /etc/containers/storage.conf

Sidebar: Let’s make sure that our sed call removed the # from the mount_progam line in the /etc/containers/storage.conf file as we asked. This is where our $mnt variable comes in very handy from our buildah mount command above. We just do a simple grep call to confirm the change:

# grep mount_program $mnt/etc/containers/storage.conf
mount_program = "/usr/bin/fuse-overlayfs"

Back to finishing up the last RUN command needed for building the container image:

# buildah run $ctr /bin/sh -c 'mkdir -p /var/lib/shared/overlay-images /var/lib/shared/overlay-layers; touch /var/lib/shared/overlay-images/images.lock; touch /var/lib/shared/overlay-layers/layers.lock' 

Setting our environment variables

Then we set our last environment variables:

# buildah config --env _BUILDAH_STARTED_IN_USERNS="" --env BUILDAH_ISOLATION=chroot $ctr

Saving our image

And finally, we need to save our image. We can do that with a buildah commit command:

# buildah commit $ctr buildahupstream

Creating and running the Bash script

At this point, we’ve created the equivalent container image as if we’d created it using the Dockerfile. Now, running each of these commands by hand is certainly doable, but sometimes it’s nice to use a script. Just grab the above commands and put them into a Bash script. Let’s call it build_buildah_upstream.sh:

#!/usr/bin/env bash
# build_buildah_upstream.sh 
#
ctr=$(buildah from fedora)
buildah config --env GOPATH=/root/buildah $ctr
buildah run $ctr /bin/sh -c 'dnf -y install --enablerepo=updates-testing \
     make \
     golang \
     bats \
     btrfs-progs-devel \
     device-mapper-devel \
     glib2-devel \
     gpgme-devel \
     libassuan-devel \
     libseccomp-devel \
     git \
     bzip2 \
     go-md2man \
     runc \
     fuse-overlayfs \
     fuse3 \
     containers-common; \
     mkdir -p /root/buildah; \
     git clone https://github.com/containers/buildah /root/buildah/src/github.com/containers/buildah; \
     cd /root/buildah/src/github.com/containers/buildah; \
     make; \
     make install; \
     rm -rf /root/buildah/*; \
     dnf -y remove bats git golang go-md2man make; \
     dnf clean all' 

buildah run $ctr -- sed -i -e 's|^#mount_program|mount_program|g' -e '/additionalimage.*/a "/var/lib/shared",' /etc/containers/storage.conf

buildah run $ctr /bin/sh -c 'mkdir -p /var/lib/shared/overlay-images /var/lib/shared/overlay-layers; touch /var/lib/shared/overlay-images/images.lock; touch /var/lib/shared/overlay-layers/layers.lock' 

buildah config --env _BUILDAH_STARTED_IN_USERNS="" --env BUILDAH_ISOLATION=chroot $ctr
buildah commit $ctr buildahupstream 

And now after running chmod 755 build_buildah_upstream.sh, we can run the script simply with ./build_buildah_upstream.sh, and our buildahupstream container image will be created for us.

Running the resulting image

No matter how we built our image, we can then run it to test out the upstream version of Buildah using Podman, with commands like:

$ podman run buildahupstream buildah version
$ podman run buildahupstream bash -c "buildah from busybox; buildah images"

Advantages of building your own

So what’s the advantage of all of this work? First, is the ability to check your progress as you go along. As we saw, the buildah mount command can be especially useful here, allowing you to mount the container’s root filesystem, which gives you access to it from the host so you can verify that your bits are in all the right places as you run through each of the commands. Second, even though the examples come from the root account, you could easily run them from a non-root user, too.

One thing to remember is in rootless mode all commands have to be done in the user namespace of the user.  You can enter the user namespace using the buildah unshare command.  If you don’t do this, the buildah mount, command will fail.  After entering the user namespace the user is allowed access to the containers root file system as a non-root user. To execute the script as a non root user, you can execute buildah unshare build_buildah_upstream.sh.

But, you may ask: "If it’s all in a script, what’s the difference between doing this and using a Dockerfile to run a straight buildah bud command?" There are a few advantages that I can see. The first is if you’re building the container image for the first time, you can build each step singularly from the command line. If there’s something wrong with a particular command you’ll find out right away and you don’t have to adjust the Dockerfile and rerun from scratch. You can just adjust the command that failed until you get it right, no restarting the entire container image build process.

When you build using a Dockerfile, it’s a fire and forget type of thing. Once you press the button, the build either completes successfully, or it fails and you have to restart from the beginning (granted, the second time through with a Dockerfile benefits you speed-wise due to cached layers). There’s also no way to get input into the build process if queried.

We recently had someone ask how they could get a response back to a script that was looking for input as they ran their Dockerfile. The simple answer is that you can’t. If you have a script or process that needs a response to a question like, "Is this OK (y|n)?", the Dockerfile just hangs or errors. If you’re running a script to do the build, you have the full power of Bash to create all sorts of scripting, especially its if-then-else syntax. That way, you could use some shell magic to answer the question, or have the script pause and enter it in yourself. Ditto if you're doing the build from the command line, you could answer the question directly.

Wrapping up

There you have it. With Buildah, building container images from the command line or a shell script can easily be done in lieu of a Dockerfile. Doing this allows you to build your container image block by block—much like some of my family’s favorite LEGO kits—with full control of the process. With these techniques, you can gain flexibility for your container development process and create more tools for your development arsenal.

New to containers? Download the Containers Primer and learn the basics of Linux containers. 

What to read next

Topics:   Containers  
Author’s photo

Tom Sweeney

Software engineer at Red Hat working on containers focusing on the Buildah and Podman projects. Manages the buildah.io and podman.io websites and can be found on freenode at #buildah and #podman. More about me

Related Content

OUR BEST CONTENT, DELIVERED TO YOUR INBOX