Building a Python Flask App with Gunicorn and PostgreSQL as a Database Backend

Introduction

Hi Folks! In this post you will learn how to create from scratch your own application that will use the database backend. In my case I choose Python as a programming language together with Flask and Gunicorn and as a database I used PostgreSQL. Python Flask stands out as a lightweight web framework that helps to create web applications. When it comes to deploying these applications in a production environment, Gunicorn steps in as a WSGI HTTP server that ensures reliable and high-performance application serving. So it is more than welcome to use those both together. On top of it I will integrate a PostgreSQL database which takes my application to the next level by providing a secure and scalable data storage solution. PostgreSQL is now the most advanced open-source database management system used in globe so this is why I chosen particularly that DB system. In some places people call it PostgreSQL but in the others, it’s simply called Postgres – don’t be confused – both is the same thing.

Application Architecture

Please take a look on the below diagram how the application components are communicating with each other.

In above design:

  1. The “User’s Browser” interacts with the Gunicorn server, which acts as the WSGI HTTP server.
  2. Gunicorn handles incoming HTTP requests and passes them to the Flask application.
  3. The Flask application processes these requests and communicates with the PostgreSQL database as needed and sends responses back.
  4. The PostgreSQL database stores and retrieves data for our Flask application.

Prerequisites

In order to make it happen consider the following items to successfully deploy and run the application:

  • PostgreSQL database instance: it can be standard installation in Linux or containerized within docker or K8s
  • python3-pip installed with all requirements (gunicorn, flask and psycopg2-binary)

PostgreSQL database instance

I did the installation of PostreSQL database within the Kubernetes and expose the service externally with a NodePort 30543 so I can reach and connect to my database. Below you can see also persistent volume for PostreSQL instance and also ConfigMap that I used to configure some parameters of PostreSQL DB. All objects are created in a dedicated namespace (app) to provide resource separation. Here is the final YAML file:

apiVersion: v1
kind: Namespace
metadata:
  name: app
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: postgres-config
  namespace: app
  labels:
    app: postgres
data:
  POSTGRES_DB: "app_db"
  POSTGRES_USER: "student"
  POSTGRES_PASSWORD: "yourSecurePassGoesHere"
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: postgres-pv
  namespace: app
spec:
  storageClassName: ""
  capacity:
    storage: 5Gi
  accessModes:
  - ReadWriteOnce
  hostPath:
    path: "/postgres-db"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: postgres-pv-claim
  namespace: app
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: app
  labels:
    app: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      nodeName: worker01
      containers:
      - name: postgres
        image: postgres:9.6.2
        env:
        - name: "POSTGRES_DB"
          valueFrom:
            configMapKeyRef:
              key: "POSTGRES_DB"
              name: "postgres-config"
        - name: "POSTGRES_USER"
          valueFrom:
            configMapKeyRef:
              key: "POSTGRES_USER"
              name: "postgres-config"
        - name: "POSTGRES_PASSWORD"
          valueFrom:
            configMapKeyRef:
              key: "POSTGRES_PASSWORD"
              name: "postgres-config"
        ports:
          - containerPort: 5432
            name: postgres
        volumeMounts:
          - name: postgres-storage
            mountPath: /var/lib/postgresql/db-data
      volumes:
        - name: postgres-storage
          persistentVolumeClaim:
            claimName: postgres-pv-claim
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: app
spec:
  ports:
  - nodePort: 30543
    port: 5432
    protocol: TCP
    targetPort: 5432
  selector:
    app: postgres
  type: NodePort

To apply the configuration execute:

$ kubectl apply -f postgress-app.yaml

Once all objects are created let’s validate if they are up and running.

$ kubectl -n app get all

Output is ok so I have my postgres deployment up and running as well as service, so all is fine. Nevertheless to be 100% sure I also tested the connection to my DB using details provided in ConfigMap just to validate the connection is ok:

$ psql -h 192.168.1.100 -p 30543 -U student -d app_db

DB is working and credentials are also fine so we are ready to go to python prerequisites section.

Python and Pip modules

To proceed with the python installation we need to have a python and python3-pip as well as psql packages installed. Python packages are used pure for creating and executing the Python app code and postgresql-client package is specifically to have psql tool to connect to the DB itself. We need to install also all required pip modules to handle flask, gunicorn to expose our application via HTTP and psycopg2 to connect to the PostgreSQL database from Python app. All can be installed by executing the following lines:

$ sudo apt-get update
$ sudo apt-get install python3-pip postgresql-client -y
$ pip install gunicorn flask psycopg2-binary

Create the Python Application

Application itself will contain a simple logic and would be used to store all available courses with option to add some only to present the mechanism between database backend and Python flask frontend part.

My Python code of the application is as follows:

import psycopg2
import subprocess
import json
from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/test')
def test():
    return render_template('test.html', data='This is a test message in test page')

@app.route('/db_init')
def db_init():
    try:
        print ("Database initializing...")
        with open('/input/postgres-ip','r') as file:
            text = " ".join(line.rstrip() for line in file)
        conn = psycopg2.connect(database="app_db",
                        host=str(text),
                        user="student",
                        password="yourSecurePassGoesHere",
                        port="30543")
        cursor = conn.cursor()
        cursor.execute("""CREATE TABLE courses(
            course_id SERIAL PRIMARY KEY,
            course_name VARCHAR (50) UNIQUE NOT NULL,
            course_instructor VARCHAR (100) NOT NULL,
            topic VARCHAR (20) NOT NULL);
            """)
        print ("Database initialized successfully")
        conn.commit

        print ("Inserting data into table courses")
        cursor.execute("INSERT INTO courses(course_name, course_instructor, topic) VALUES('Linux System Administration','Marcin Kujawski','Linux');");
        cursor.execute("INSERT INTO courses(course_name, course_instructor, topic) VALUES('Kubernetes Administration','Marcin Kujawski','Kubernetes');");
        cursor.execute("INSERT INTO courses(course_name, course_instructor, topic) VALUES('Linux for System Engineers','Marcin Kujawski','Linux');");
        conn.commit
        print ("Data inserted into DB table")

        print ("Getting database content...")
        dbContent = []
        cursor.execute('SELECT * FROM courses;')
        rows = cursor.fetchall()
        conn.commit()
        cursor.close()
        conn.close()
        #for row in rows:
        #    dbContent.append(row)

        return render_template('init.html', data='Database successfully initialized')
    except:
        return render_template('init.html', data='Database already exist, doing nothing')

@app.route('/db_get')
def db_get():
    try:
        with open('/input/postgres-ip','r') as file:
            text = " ".join(line.rstrip() for line in file)
        conn = psycopg2.connect(database="app_db",
                        host=str(text),
                        user="student",
                        password="yourSecurePassGoesHere",
                        port="30543")
        cursor = conn.cursor()
        #dbContent = []
        cursor.execute('SELECT * FROM courses;')
        rows = cursor.fetchall()
        conn.commit()
        cursor.close()
        conn.close()
        #for row in rows:
        #    dbContent.append(row)

        return render_template('get.html', data=rows)

    except:
        return render_template('init.html', data='Database is not yet initialized, please navigate to Application -> Init')

@app.route('/db_add')
def db_add():
    return render_template('add.html')

@app.route('/db_add', methods=['POST'])
def db_add_post():
    course = request.form['course']
    trainer = request.form['trainer']
    category = request.form['category']

    try:
        with open('/input/postgres-ip','r') as file:
            text = " ".join(line.rstrip() for line in file)
        conn = psycopg2.connect(database="app_db",
                        host=str(text),
                        user="student",
                        password="yourSecurePassGoesHere",
                        port="30543")
        conn.autocommit = True
        cursor = conn.cursor()
        
        insert_query = ''' INSERT INTO courses(course_name, course_instructor, topic) VALUES (%s,%s,%s);'''
        record = (course, trainer, category)
        cursor.execute(insert_query, record)
        print("INSERT INTO courses(course_name, course_instructor, topic) VALUES('" + course + "','" + trainer + "','" + category  + "');")
        count = cursor.rowcount
        print(count, "Record inserted successfully into table")
        
        cursor.close()
        conn.close()

        return render_template('init.html', data='New course ' + course + ' (trainer: ' + trainer  + ') in category ' + category + ' was added to database.')

    except:
        return render_template('init.html', data='An error occurred when adding new course.')

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Here is the description of the code used above:

  • Define the Flask application:
app = Flask(__name__)
  • Function index() to handle root URL (/) of the application and render the index.html page from templates folder:
@app.route('/') 
def index():
  • Function test() to handle /test URL of the application and render the test.html page from templates folder:
@app.route('/test') 
def test():
  • Function db_init() to handle /db_init URL of the application and initialize the database. Initialization tasks are actually create a table and insert predefined 3 entries inside the table. As a final result function is rendering the init.html page from templates folder showing the status of the initialization:
@app.route('/db_init')
def db_init():
  • Function db_get() to handle /db_get URL of the application and get all entries from the database. All rows retrieved by the function are displayed in rendered get.html page from templates folder showing all table rows within fancy HTML table:
@app.route('/db_get')
def db_get():
  • Function db_add() to handle /db_add URL of the application and redirect to rendered add.html page from templates folder where HTML form is displayed:
@app.route('/db_add')
def db_add():
  • Function db_add_post() to handle /db_add URL with POST method that is triggered by submit button in add.html page. Values from all 3 input fields are gathered and used to insert a new entry into database. As a final result function is rendering the init.html page from templates folder showing the status of adding new course into database:
@app.route('/db_add', methods=['POST'])
def db_add_post():
  • Start the Flask application running on all interfaces on port 5000 with enabled debug:
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)

Ok, once we have the Python application ready and database is running and accessible, we can start the whole application with command:

$ python3 app.py

You should receive the output similar to mine:

It shows that we are running application on all interfaces, both localhost and NIC address are used. What’s more we are informed that Gunicorn is a development server only and we should use the other WSGI server while doing that on prod. We know that.

To access my application I need to go to URL: http://192.168.1.120:5000. As expected I see my app GUI presenting home page.

Let’s now test this app a little bit and go to each app routing specified in code. First would be a /test. As shown below I click on menu item “Test Page” that transfers me to the /test URL and I can see that test message appears so I treat this as working.

Next, let’s click on menu and navigate into “Application” -> “Get courses”.

I was moved to the /db_get URL that is showing me the information that database of my application was not yet initialized and I need to go first into “Application” -> “Init”.

Let’s do this. I clicked on “Application” -> “Init” button from my top menu and I was moved to /db_init URL accordingly. All tasks covered in this app routing were executed and finally I got the message that DB was initialized successfully.

Ok, so now I need to get back to the “Application” -> “Get courses” to get all entries from table “courses” from my DB. Let’s check this.

Everything is fine – I see 3 predefined courses that were added during the initialization of the table.

Last thing to check is adding a new course. In order to validate this activity I need to go to “Application” -> “Add course”. Here we go, I see the input fields I need to fill in and finally I click on button “Add Course”.

I have added new course with details:

  • Course name: Bash programming
  • Trainer name: Jack Sparrow
  • Category: Linux

I was prompted that new course was added successfully into the database.

Perfect! So the last validation would be to list all courses. All entries should include my new course added one step before. Let’s again navigate to “Application” -> “Get courses” which is triggering /db_get app route and observe what we have here.

As expected, instead of 3 courses I have 4. This proves that application is working end-to-end and all app routes are doing job well.

If you want to look into the Python Flask logs, you can find all Python “prints” as well as each of the HTTP request I have done by searching the HTTP codes and method executions or by specific URL (app route). I can easily identify when I click what based on the below log output.

Log analysis concludes the application testing and also this blog post.

To summarize it – we have managed to deploy a PostreSQL server running database. What’s more we developed the Python Flask application with Gunicorn that is exposed on port 5000 and handles API calls based on some example app routes/URLs we have created that corresponds to specific application activity with correlation to external database, like:

  • Presenting the “Home page”
  • Application initialization (CREATE)
  • Get courses (SELECT)
  • Add course (INSERT)

We have tested all of them and we know now how to organize and code it.

Conslusion

In conclusion, we had an exciting journey to build a powerful and scalable Python Flask web application with Gunicorn as the application server and PostgreSQL as the database backend. Throughout this article, we’ve explored the key steps required to set up this stack from scratch.

I have covered topics such as database deployment (within Kubernetes), creating RESTful API endpoints inside our application, and dealing with the PostgreSQL database, allowing you to create, store and retrieve and insert data efficiently. Flask together with Gunicorn (or any WSGI server) provides a solid foundation for your projects, offering simplicity, reliability, and scalability.

I hope this article provided you with the knowledge and confidence to get started on your web development journey. It is not so hard to build your own app, just believe in yourself. Happy coding!