Build a bootable Python Flask application using RHEL Image Mode with Podman Desktop

Use Podman Desktop to create a bootable Flask-based application using image mode for RHEL. We will integrate Flask, Gunicorn, and NGINX into a bootable container.

Download Podman Desktop

This lesson will walk you through developing a basic Python Flask application as a bootable container and deploying it as a web page using Gunicorn and NGINX. We’ll set up basic code and configuration files and build a Containerfile which we will use to create our bootable container.

To get the full benefit of this lesson, you must:

In this lesson, you will:

  • Develop a foundational understanding of the core application components.
  • Create a basic Hello World application using Python and Flask.
  • Create configuration files for Flask, Gunicorn, and NGINX.
  • Create a Containerfile to install the necessary components and configure a bootable container.

The core application components

This application is made up of three key components: Flask, Gunicorn, and NGINX.  

  • Flask: In this architecture, Flask provides a lightweight and flexible web framework for the Python programming language. It’s left to Flask to provide the routing (matching web addresses to Python functions), request handling, and templating basics that allow you to build your web applications in a Pythonic way. We chose Flask for this example because it has a simple core that's quick to learn and use, but it’s also extensible and well-supported by the Python community.
  • Gunicorn: Green Unicorn (Gunicorn) is the Python web server gateway interface (WSGI) HTTP server used in this architecture. Flask doesn’t know how to directly talk to the web. Instead, it uses WSGI, a standard interface. Gunicorn acts as the bridge, taking requests from the web and translating them into a format the Python application (e.g., built with Flask) can understand. In our design, it takes the application's response and sends it back to NGINX, which communicates with the client.
  • NGINX: In this architecture, NGINX acts as a frontline webserver and reverse proxy server. This component faces the external network, receiving all incoming web traffic (HTTP requests) from your browser. NGINX does not handle requests processed by your Python application (dynamic content). Instead, it forwards (proxies) these requests to Gunicorn, which calls Flask. In addition, NGINX can handle SSL/TLS termination, encrypting and decrypting traffic, and it can implement various security measures and traffic management policies. You can refer to the RHEL documentation for NGINX.

Create a basic Hello World application using Python and Flask

We’ll start off by creating a configuration directory in which to store our work. For our example, we’ll call this directory rhel-flask-bootc where we’ll store all the configuration files created in this lesson. In a production development environment, these would be kept under source control:

mkdir rhel-flask-bootc

We’re now ready to start developing our Python Flask application. For the purposes of this demo, a simple "Hello World" app will get us started. Start by creating the file helloworld.py with your favorite text editor and saving it into our rhel-flask-bootc directory.  

The contents of this file will:

  1. Load the Flask module.
  2. Associate the application with the default webserver home directory.
  3. Create a hello_world function that returns the string "Hello, World".

The file content is as follows:

from flask import Flask

app = Flask(__name__)

@app.route('/')

def hello_world():

    return 'Hello, World!'

Gunicorn is the WSGI server that will serve our Flask application. To serve up our application as a web page, we’ll need our Gunicorn to call our Hello World application in the wsgi.py file, which we will also place in the rhel-flask-bootc directory as follows: 

from helloworld import app

if __name__ == "__main__":
    app.run()

Create configuration files for Flask, Gunicorn, and NGINX

We’ll need runtime configuration files for Flask, Gunicorn, and NGINX, which we will include in our image mode container. Let’s start by creating a systemd unit file to start the Gunicorn server for our Flask application. 

We’ll call the flask-app.service file. Once system networking begins, this will start the Gunicorn server and invoke our WSGI-based Flask application. Note that the RunTimeDirectory directive instructs systemd to create a directory flask-app under /run for the UNIX domain socket used in communications between Gunicorn and NGINX. We’ll come back to that socket later as SELinux requires us to explicitly allow communications between Gunicorn and NGINX over this channel. 

For now, we’ll create the flask-app.service file in the rhel-flask-bootc directory with our other configuration files as follows: 

After=network.target

[Service]
User=nginx
Group=nginx
RuntimeDirectory=flask-app
WorkingDirectory=/app
Environment="PATH=/app/"
ExecStart=/usr/local/bin/gunicorn --workers 3 --bind unix:/run/flask-app/flask-app.sock -m 007 wsgi:app

[Install]
WantedBy=multi-user.target

Next, we will configure our NGINX server to communicate with Gunicorn. The file will specify the server name and port (that we plan to serve our web services on) as to how to communicate with our Gunicorn server. For this example, we used localhost as the name. But for production, we recommend specifying the server name using https and exposing our web service on a high-numbered port such as 8080. 

This content should be placed in the flask-app.conf file, which we’ll place in our rhel-flask-bootc directory as follows:

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_pass http://unix:/run/flask-app/flask-app.sock;
    }
}

As a final step, we will create an SELinux type enforcement file to allow NGINX to communicate with Gunicorn. Type enforcement is a file format of SELinux, not a type of SELinux enforcement file. We’ll name this file nginx_connect_flask_sock.te and place it in our rhel-flask-bootc directory again as follows:

module nginx_connect_flask_sock 1.0;

require {
	type httpd_t;
	type unconfined_service_t;
	class unix_stream_socket connectto;
}

#============= httpd_t ==============
allow httpd_t unconfined_service_t:unix_stream_socket connectto;

Create a Containerfile to install the components and configure a bootable container

Starting with the RHEL 10 bootable image, the sample ContainerFile performs the following steps:

  1. Install the required RPM packages through DNF.
  2. Copy the files we’ve created into our container from our container directory (rhel-flask-bootc).
  3. Use pip to install Gunicorn and Flask modules from the Python community repository.
  4. Configure NGINX to communicate with Gunicorn.
  5. Configure SELinux to allow NGINX to communicate with Gunicorn (over UNIX domain sockets).
  6. Enable and start our application services.

Using the text editor of your choice, create this file and save it locally in the rhel-flask-bootc/ directory where you placed your other files.

Later, you will upload this Containerfile to Podman Desktop when you build the image:

# Start with the RHEL 10 bootable base image
FROM registry.redhat.io/rhel10/rhel-bootc

# Install necessary packages from RHEL Application Streams using dnf
# This includes default Python 3, pip, and Nginx
RUN dnf install -y \
    python3 \
    python3-pip \
    nginx && \
    dnf clean all && rm -rf /var/cache/dnf

# Copy your Flask application files and Gunicorn configuration
# Assuming your app files (helloworld.py, gunicorn_config.py)
# are in the same directory as your Containerfile
COPY helloworld.py /app/
COPY wsgi.py /app/
COPY nginx_connect_flask_sock.te /app/
COPY flask-app.service /etc/systemd/system/

# Set the working directory
WORKDIR /app

# Install Flask and Gunicorn via pip
RUN pip3 install --no-cache-dir flask gunicorn

# --- Nginx Configuration (Example Adaptation) ---
# Copy a custom Nginx configuration file that acts as a reverse proxy
# This file needs to be created separately and configured to forward requests
# to the Gunicorn process (e.g., listening on localhost:80)
COPY flask-app.conf /etc/nginx/conf.d/flask-app.conf

# Ensure nginx can talk to gunicorn
RUN checkmodule -M -m /app/nginx_connect_flask_sock.te -o /app/nginx_connect_flask_sock.mo
RUN semodule_package -o nginx_connect_flask_sock.pp -m nginx_connect_flask_sock.mo
RUN semodule -i nginx_connect_flask_sock.pp
RUN mkdir /run/flask-app && chgrp -R nginx /run/flask-app && chmod 770 /run/flask-app
RUN semanage fcontext -a -t httpd_var_run_t /run/flask-app

# Enable our application services
RUN systemctl enable nginx.service
RUN systemctl enable flask-app.service

Now that you have your configuration files, let’s move on to the next lesson where we’ll create the disk image.

Previous resource
Access the Red Hat Container Registry
Next resource
Build and run a bootable Flask application disk image in Podman Desktop