No-Cost RHEL Developer Subscription now available

In my previous article, Run Red Hat Enterprise Linux 8 in a container on RHEL 7, I showed how to start developing with the latest versions of languages, databases, and web servers available with Red Hat Enterprise Linux 8 even if you are still running RHEL 7. In this article, I’ll build on that base to show how to get started with the Flask microframework using the current RHEL 8 application stream version of Python 3.

From my perspective, using Red Hat Enterprise Linux 8 application streams in containers is preferable to using software collections on RHEL 7. While you need to get comfortable with containers, all of the software installs in the locations you’d expect. There is no need to use scl commands to manage the selected software versions. Instead, each container gets an isolated user space. You don’t have to worry about conflicting versions.

In this article, you’ll create a Red Hat Enterprise Linux 8 Django container with Buildah and run it with Podman. The code will be stored on your local machine and mapped into the container when it runs. You’ll be able to edit the code on your local machine as you would any other application. Since it is mapped via a volume mount, the changes you make to the code will be immediately visible from the container, which is convenient for dynamic languages that don’t need to be compiled. While this approach isn’t the way to do things for production, you get the same development inner loop as you’d have when developing locally without containers. The article also shows how to use Buildah to build a production image with your completed application.

Additionally, you’ll set up the Red Hat Enterprise Linux 8 PostgreSQL application stream in a container that is managed by systemd. You can use systemctl to start and stop the container just as you would for a non-container installation.

Install Podman and Buildah on Red Hat Enterprise Linux 7

First, we need to install Podman, which is in the extras repo on Red Hat Enterprise Linux 7. The extras repo isn’t enabled by default. Developers should also enable the rhscl (Red Hat Software Collections), devtools, and optional repos:

$ sudo subscription-manager repos --enable rhel-7-server-extras-rpms \
    --enable rhel-7-server-optional-rpms \
    --enable rhel-server-rhscl-7-rpms \
    --enable rhel-7-server-devtools-rpms

Now install Podman and Buildah. If sudo isn’t set up on your system, see How to enable sudo on Red Hat Enterprise Linux.

$ sudo yum install podman buildah

Later, we’ll run containers with systemd. If SELinux is enabled on your system (it is by default), you must turn on the container_manage_cgroup boolean to run containers with systemd:

$ sudo setsebool -P container_manage_cgroup on

For more information, see the containers running systemd solution.

Note: The Red Hat ID created when you joined Red Hat Developer gives you access to content on the Red Hat Customer Portal.

Set up a Flask example app

We need Flask code to run. Let’s use Flaskr, the sample app in the Flask distribution's examples/tutorial directory. Download Flask into a working directory on the host machine and extract the tutorial app:

$ sudo mkdir /opt/src
$ sudo chown $USER:$USER /opt/src
$ cd /opt/src
$ mkdir flask-app
$ curl -L https://github.com/pallets/flask/archive/1.1.1.tar.gz | tar xvzf - 
$ cp -pr flask-1.1.1/examples/tutorial flask-app

We’ve now got an example Flask app at /opt/src/flask-app.

Run Python 3.6 and Flask in a Red Hat Enterprise Linux 8 container (manually)

Now we need Python 3.6 and Flask. We’ll manually set up a container with the dependencies and then run the app to see how it’s done. Let's start with the Red Hat Enterprise Linux 8 Universal Base Image (UBI). If you're unfamiliar with the RHEL UBIs, see the section "Red Hat Universal Base Images."

Red Hat has a new container registry which uses authentication: registry.redhat.io. A Red Hat account isn’t required to use UBI images, but other Red Hat images that aren’t part of UBI can only be obtained through registry.redhat.io. The Red Hat ID created when you joined Red Hat Developer gives you access to the Red Hat Container Registry, so for simplicity, I use only registry.redhat.io in this example.

If you aren’t logged in when you try to pull an image, you’ll get a verbose error message:

...unable to retrieve auth token: invalid username/password.

Log in with your Red Hat username and password:

$ sudo podman login registry.redhat.io

Note: Podman was designed to run without root. However, the support for this feature isn’t there with Red Hat Enterprise Linux 7.6. For more information, see Scott McCarty’s, A preview of running containers without root in RHEL 7.6.

Now run the container, making our source directory /opt/src available inside the container and exposing port 5000 so you can connect to the Flask app with a browser on the host system:

$ sudo podman run -v /opt/src:/opt/src:Z -it -p 5000:5000 registry.redhat.io/ubi8/ubi /bin/bash

The previous command also invoked an interactive shell for the Red Hat Enterprise Linux 8 based UBI container. From inside the container, see what application streams are available with RHEL 8:

# yum module list

You might notice an extra group of application streams labeled Universal Base Image. See the UBI section for more information about Red Hat Universal Base Images.

Next, install Python 3.6:

# yum -y module install python36

Python 3.6 is now installed in our container and is in our path as python3, not python. If you want to know why see Petr Viktorin’s article, Python in RHEL 8.

Next, use pip to install Flask:

# pip3 install flask

You’ll get a warning about running pip as root. Running pip as root on a real system is generally a bad idea. However, we’re running in a dedicated container which is isolated and disposable, so we can do pretty much whatever we want with files in /usr.

Let’s check where the Flask command-line interface (CLI) was installed:

# which flask

Pip installed it into /usr/local/bin.

Now let’s run the example app inside of the container:

# cd /opt/src/flask-app
# export FLASK_APP=flaskr
# export FLASK_ENV=development
# flask init-db
# flask run --host=0.0.0.0

Using a browser on the host system, go to http://localhost:5000/ and view the resulting page:

Now, you’ve got a container configured by hand that runs Flask applications using Red Hat Enterprise Linux 8’s Python 3.6 application stream on your RHEL 7 system. You could treat this container like a "pet," and use podman restart -l and podman attach -l when you want to run it again—as long as you don’t delete it. We didn’t name the container, but the -l conveniently selects the last running container. Alternatively, you’d need to use podman ps -a to get the ID, or randomly generated name to pass to podman restart and podman attach.

When you restart the container, it is similar to rebooting a system. The installed files are there, but any of the other runtime state-like environment variable settings won't persist. The life cycle for containers you’ve seen in most tutorials is "run then delete" since containers are designed to be ephemeral. However, knowing how to create and restart containers can be handy when you need to experiment.

Create a Flask container image with Buildah

To make things easier, we’ll create a container image that has Flask installed and starts the Flask app anytime the container is run. The container won’t have a copy of the app, we’ll still map the app into the container from the host system. The code will be stored on your local machine where you can edit it as you would any other application source. Because it is mapped via a volume mount, the changes you make to the code will be immediately visible inside the container.

When creating images with Buildah, you can use Dockerfiles or Buildah command lines. For this article, we’ll use the Dockerfile approach because you’ve probably seen it before in other tutorials.

Because we are working with files that are shared between your host system and the container, we’ll run the container using the same numeric user ID (UID) as your regular account. While inside the container, any files are created in the source directory are owned by your user ID on the host system. Find out your UID with the id command:

$ id

Make a note of the number after UID= and GID= at the start of the line. On my system, my UID and GID are both 1000. In the Dockerfile and other examples here, change the USER line to match your UID:GID.

In /opt/src/flask-app, create Dockerfile with the following contents:

FROM registry.redhat.io/ubi8/python-36

RUN pip3 install flask

# set default flask app and environment
ENV FLASK_APP flaskr
ENV FLASK_ENV development

# This is primarily a reminder that we need access to port 5000
EXPOSE 5000

# Change this to UID that matches your username on the host
# Note: RUN commands before this line will execute as root in the container
# RUN commands after will execute under this non-privileged UID
USER 1000

# Default cmd when container is started
# Create the database if it doesn't exist, then run the app
# Use --host to make Flask listen on all networks inside the container
CMD [ -f ../var/flaskr-instance/flaskr.sqlite ] || flask init-db ; flask run --host=0.0.0.0

A note on the Dockerfile: Instead of installing Python 3.6, I used a UBI image from Red Hat that already had Python 3.6 on top of the UBI 8 image. The command that runs when the container starts will create the database if it doesn’t exist, and then run the Flask app.

Next, build the Flask container (don’t forget the trailing .):

$ sudo buildah bud -t myorg/myflaskapp .

Now we can run the Flask container containing our app:

$ sudo podman run --rm -it -p 5000:5000 -v /opt/src/flask-app:/opt/app-root/src:Z myorg/myflaskapp

The Flaskr app should now be running, which you can verify by using a browser on the host system and going to http://localhost:8000/ to view the resulting page.

You can now edit the code in /opt/src/flask-app like you would any regular source code. When you need to restart Flask, Ctrl+C the container. Note the --rm in the run command, which automatically removes the container when it exits.

To start the container again, you will need to use the above podman run command again, which creates a fresh new container, plus a new database with nothing in it. For many situations, this fresh start is desirable.

Persist the SQLite database between containers

The Flaskr example uses a SQLite database, which is stored inside the container. Containers are intended to be ephemeral, so any changes made inside the container will be lost when the container is deleted.

There are several ways you can keep the database (or other files) from containers across runs. As mentioned above, you could try to keep the container around and restart it, instead of recreating it with run every time. While that practice can be handy for experimenting and debugging, this isn’t a good way to accomplish persistence. Now is a good time to mention if you do have changed files you’d like to get out of a container that has exited but hasn’t been removed, Podman and Buildah have a handy mount command that mounts the container on the host system so you can access the files through the filesystem.

Note: If you are confused about the difference between a container and a container image, see Scott McCarty’s article: A Practical Introduction to Container Terminology.

Instead of trying to keep the container around, a much cleaner solution is to arrange for the database (or other files you’d like to persist) to be stored in the host's filesystem. You can do this by adding another volume mount with -v to the run command. Here’s the full command, which stores the database with the source code:

$ sudo podman run --rm -it -p 5000:5000 -v /opt/src/flask-app:/opt/app-root/src:Z \
    -v /opt/src/flask-app/instance:/opt/app-root/var/flaskr-instance:Z myorg/myflaskapp

Run MariaDB in a container

Another way to deal with persistence is to run a database server in another container. In a previous article, Run Red Hat Enterprise Linux 8 in a container on RHEL 7, I showed how to run MariaDB using the current Red Hat Enterprise Linux 8 application stream on a RHEL 7 system. The MariaDB container is managed by systemd, so you can use systemctl commands just like you would for a non-containerized version.

For the sake of brevity, I won’t replicate the instructions to get MariaDB running in this article, just follow the previous article's MariaDB section to get that database running.

The one thing you’ll need to know is how to make your Flask container connect to the database container. By default, containers are designed to run with an isolated virtual network. Steps need to be taken to network containers together. I think the easiest approach for the scenario in this article—where you just want to run a few containers—is to arrange for the containers to share the host’s network.

To use the host's network, add --net host to the run command for both your Flask and database containers. If you are using the host’s network, you won’t need to select which ports to expose. So, the full run command for the Flask container is:

$ sudo podman run --rm -it --net host -v /opt/src/flask-app:/opt/app-root/src:Z \
    -v /opt/src/flask-app/instance:/opt/app-root/var/flaskr-instance:Z myorg/myflaskapp

While using the host’s network is quick and easy for development, you’d run into port conflicts if you had a number of MariaDB containers that all wanted to use port 3306. One way to improve this setup is to use Podman’s pod capabilities to put the app and database containers in the same pod, where they share namespaces. See Brent Baude’s article, Podman: Managing pods and containers in a local container runtime.

Use Buildah to create an image with your Flask app

After you’ve developed your app, you can use Buildah to create a distributable container image with your Flask app. We’ll use Buildah command lines instead of a Dockerfile. This approach is much more flexible for complex builds and automation: You can use shell scripts or whatever other tools you use for your build environment.

In /opt/src/flask-app, create app-image-build.sh with the following contents:

#!/bin/sh
# Build our Flask app and all the dependencies into a container image
# Note: OOTB on RHEL 7.6 this needs to be run as root.

MYIMAGE=myorg/myflaskapp
FLASK_APP=flaskr
FLASK_ENV=development
USERID=1000

IMAGEID=$(buildah from ubi8/python-36)
buildah run $IMAGEID pip3 install flask

buildah config --env FLASK_APP=$FLASK_APP --env FLASK_ENV=$FLASK_ENV $IMAGEID

# any build steps above this line run as root inside the container
# any steps after run as $USERID
buildah config --user $USERID:$USERID $IMAGEID

buildah copy $IMAGEID . /opt/app-root/src
buildah config --cmd '/bin/sh run-app.sh' $IMAGEID

buildah commit $IMAGEID $MYIMAGE

This image calls a start script to launch our application. Next, create run-app.sh in the same directory, with the following contents:

#!/bin/sh

APP_DB_PATH=${APP_DB_PATH:-../var/instance/flaskr.sqlite}

if [ ! -f ${APP_DB_PATH} ]; then
echo Creating database
flask init-db
fi

echo Running app $FLASK_APP
flask run --host=0.0.0.0

Now, build the image:

$ sudo app-image-build.sh

Run and test the new image:

$ sudo podman run --rm -it --net host -v /opt/src/flask-app/instance:/opt/app-root/var/flaskr-instance:Z myorg/myflaskapp

When you are ready, you can distribute your application by pushing it to a container registry like Red Hat’s Quay.io.

Next steps

By now, you should see that it is easy to get the software components you need running in containers so you can focus on development. It shouldn’t feel very different from developing without containers.

The Flask container you built isn’t tied to a specific app. You could reuse that container for other Flask apps by overriding the environment variables: add -e FLASK_APP mynewapp to the podman run command.

You could also build on the Dockerfile above to install more Python modules for your app into your container image, or customize the way the app starts.

Check out what other UBI 8 images are available in the Red Hat Container Catalog. If the language, runtime, or server aren’t available as a UBI image, you can build your own beginning with the ubi8 base image. Then, you can add the application streams and other rpms you need with yum commands in a Dockerfile, or with buildah run.

Red Hat Universal Base Images

I’ve mentioned Universal Base Images (UBIs) a number of times in this article without explaining them. Red Hat provides these UBIs to use as a base for your container images. From Mike Guerette’s article, Red Hat Universal Base Image: How it works in 3 minutes or less:

"Red Hat Universal Base Images (UBI) are OCI-compliant container base operating system images with complementary runtime languages and packages that are freely redistributable. Like previous RHEL base images, they are built from portions of Red Hat Enterprise Linux. UBI images can be obtained from the Red Hat Container Catalog and be built and deployed anywhere.

"And, you don’t need to be a Red Hat customer to use or redistribute them. Really."

With the release of Red Hat Enterprise Linux 8 in May, Red Hat announced that all RHEL 8 base images would be available under the new Universal Base Image End User License Agreement (EULA). This fact means that you can build and redistribute container images that use Red Hat’s UBI images as your base, instead of switching to images based on other distributions, like Alpine. In other words, you won’t have to switch from using yum to using apt-get when building containers.

There are three base images for Red Hat Enterprise Linux 8. The standard one is called ubi, or more precisely, ubi8/ubi. This is the image used above which you will probably use most often. The other two are minimal containers. They contain little supporting software for when image size is a high priority and a multi-service image that allows you to run multiple processes inside the container managed by systemd.

Note: There are also UBI images for Red Hat Enterprise Linux 7 under ubi7 if you want to build and distribute containers running on a RHEL 7 image. For this article, we’ll only use the ubi8 images.

If you are just starting out with containers, you don't need to delve into UBI details right now. Just use the ubi8 images to build containers based off Red Hat Enterprise Linux 8. However, you will want to understand UBI details when you start distributing container images or have questions about support. For more information, see the references at the end of this article.

More information

Related articles:

Cheat sheets:

Podman and Buildah:

UBI: 

Last updated: March 29, 2023