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 mount $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.