Complete Guide on Model Deployment with Flask and Heroku | by Tarek Ghanoum | Towards Data Science

I felt tears of joy running down my cheeks when I deployed my first Machine Learning model, and when I looked out the window, I saw a rainbow in the far distance. I took it as a sign that I will do great things in this world.
That’s at least how I imagined it would be. In reality, I spent two days dealing with every type of error that Heroku could throw at me. When I finally deployed my model, I immediately shut the computer, drank my pre-workout, and hit the gym.
This article aims to show you how to maneuver correctly when working with model deployment while showing you what to be aware of along the way. The final product can be seen here, while a more advanced deployment of the same model can be seen here (you might need some patience to visit the web apps since Heroku puts apps ‘to sleep’ after a period of inactivity — the app will ‘wake up’ with a delayed response of some seconds).
The article is split into three parts which are:
Building a Machine Learning modelSetting up a Flask web applicationDeployment on Heroku
Building a Machine Learning model
Setting up a Flask web application
Deployment on Heroku
The first step on our journey is to train a Machine Learning (ML) model, but given that my main focus is deployment, I will not put much effort into this part of the article.
I will build an XGBoost model based on data from a bike-sharing scheme. The model will predict the number of bikes rented a given day based on three features; temperature, humidity, and windspeed.
The focus at this stage is the methodology and not the model performance, so whether the model is XGBoost, RF, LG, or any other model is not important.
The code below is all you need to train and save your model. Where to store the saved model will become more evident as we move along.
At this stage, you should create an empty directory which I call ‘MyApp’, and store your model in it. I will be using Visual Studio Code (VSCode) to demonstrate the process, but you can use other methods.
Contrary to a static website, where a user only reads the page content, we want the user to interact with our webpage. Websites with functionality and interactive element are referred to as web applications.
In our case, a user will be met by three input fields, and by submitting some data, we want to give the user a prediction. Before we can do that, we need to understand that a browser interacts with other web pages by sending requests. A typical browser will send a GET request following a so-called HTTP protocol to visit a page and a POST request to send data through the web page. You will hopefully GET it along the way (pun intended)!
As python programmers, we would usually have to write a bunch of code to handle user requests. Instead of writing repetitive code to handle requests, a dude (named Armin Ronacher) just put it in a library, as a joke, and called it flask.
Flask is a web server framework because it requires our code to be organized in a certain way. Start by adding two folders called ‘Models’ and ‘Templates’ in your ‘MyApp’ directory. We will not be using the ‘Static’ folder for now, but feel free to add any optional CSS code. You should create a python file on the same root level as the folders and use the same naming convention as me ‘app.py’. Afterward, create an HTML file called index and place it in the templates directory. No worries that the app.py and index.html files are empty; focus on the framework at this stage.
Your directory should look similar to the image below. Notice that I put our ML model in the model's folder.
Before going through the files step-by-step, I will need you to create a virtual environment.
Given that flask is simply a third-party library, we can easily install flask using pip (pip install Flask) and run our web application, but a problem occurs when the web application is dependent on specific versions of python and other third-party libraries. Imagine that your web application works perfectly, but all of a sudden, an update to a third-party library comes along the way and changes the name of certain functions. This will make your program stop functioning. To get around this problem, we can use a so-called virtual environment (virtualenv).
Think of a virtual environment as a folder that is unaware of other libraries installed on your computer. Only libraries installed in the folder can be accessed.
To create a virtual environment, we will need you to install virtualenv on your current Python installation. In other words, open a terminal (or command) line and type:
pip install virtualenv
To check whether you have it installed in your system, type the following command:
virtualenv --version
It is also possible to use pyvenv instead of virtualenv, but you will have to find another guide.
Once virtualenv is installed, you can then create a virtual environment. The virtual environment files should be generated inside the ‘MyApp’ directory. You can either change your current directory using the command-line interface (using ls and cd) or right-click on your ‘MyApp’ folder and choose ‘New Terminal at folder’ (if you don't have that option — no worries, buy a Mac for 10.000$ or whatever they cost at the moment).
Type the following command, use only one of the commands depending on whether you’re using Windows or Mac:
python3 -m venv virtual (Mac)py -3 -m venv virtual (Windows)
By doing this, you just used Python and the venv library to create a folder named ‘virtual’ with all the necessary virtual environment files.
We will now have to activate the virtual environment by typing:
. virtual/bin/activate (Mac)virtual\Scripts\activate (Windows)
You can always do the opposite by simply typing deactivate. If all went well, you should see something similar to this in your command-line interface:
(virtual) <user name> myapp %
You can explore the new ‘virtual’ folder by opening and looking inside the ‘bin’ folder (or ‘Scripts’ on Windows). You should see pip and python in the folder pre-installed. Any new library installed will be found in the ‘bin’ folder.
Now we can finally download the flask library:
pip install Flask
You should be able to easily locate flask in virtual/bin/ on Mac or virtual\Scripts\ on windows:
You should find the nearest mirror at this stage and repeat the words: I can do anything if I set my mind to it.
Let’s open the ‘app.py’ file and start inserting some code. The app file is the core of your web application which will run on Heroku. It will listen for user requests on specific ports and respond with specific functions depending on the type of request.
The application can be separated into three parts. The first consists of importing libraries and initializing the flask app. We use the Flask(__name__) to tell flask where our app is located. Flask will locate templates and static files relative to the location of our app.
from flask import Flask, render_template, requestimport pickleimport pandas as pd# Initialise the Flask appapp = Flask(__name__)
Now we import our saved model using pickle from the /models directory and save it as ‘model’:
# Use pickle to load in the pre-trained modelfilename = "models/model.sav"model = pickle.load(open(filename, "rb"))
Our web application will only have a single page, which is referred to as the main or default page. The ‘/’ route is the home URL. This should, by default, point to the index.html file, i.e., the main page.
We want to trigger a specific function when a user visits the main page. For that, I have created a function called main which will respond to user requests depending on whether the request is a GET or POST.
A GET request is sent by the user when the purpose is to read or visit our main page. In this situation, I simply want to display the content of our index.html file, which we will create next. To do that, I use the render_template function which purpose is to look for a file in the templates directory and display its content.
A POST request is sent by the user when information is being sent back to the webserver. We will allow the user to insert a specific temperature, humidity, and windspeed. I will retrieve this information from the index.html file by using the request.form.get function. I have given the input fields a specific name, so the functions know where to look. I then take the data and create a DataFrame from it using pandas and then feed the data to our ML model and store the result in a prediction variable before sending it back to the index.html file to display the result.
Notice that the default app.route is configured to receive GET requests, so I added the methods=[“GET”, “POST”] in order to handle POST requests.
# Set up the main route@app.route('/', methods=["GET", "POST"])def main():    if request.method == "POST":       # Extract the input from the form       temperature = request.form.get("temperature")       humidity = request.form.get("humidity")       windspeed = request.form.get("windspeed")       # Create DataFrame based on input       input_variables = pd.DataFrame([[temperature, humidity, windspeed]],     columns=['temperature', 'humidity', 'windspeed'], dtype=float, index=['input'])       # Get the model's prediction       # Given that the prediction is stored in an array we simply           extract by indexing       prediction = model.predict(input_variables)[0]       # We now pass on the input from the from and the prediction          to the index page       return render_template("index.html", original_input {'Temperature':temperature, 'Humidity':humidity, 'Windspeed':windspeed}, result=prediction)     # If the request method is GET     return render_template("index.html")
The complete code is displayed below:
Now we will create the default page, which the user will be met by. Let’s break the index.html code into three parts, as I did in the previous walk-through. Although, I would recommend watching a simple HTML tutorial video if you have never read or written HTML code.
The first part of my index file simply consists of voluntary CSS code. As an alternative, I could have placed it in a stylesheet named ‘styles.css’ and store it in the static directory. I do this in the more advanced deployment (GitHub).
<head>   <!-- CSS code -->   <style>     form {          margin: auto;          width: 35%;          }     .result {          margin: auto;          width: 35%;          border: 1px solid #ccc;             }    </style>    <title>Bike Usage Model</title></head>
The second part consists of a form with three input fields. Notice the ‘required’ attribute, which assures input from the user. The name attribute is used as a reference in the app.py file when I used the request.form.get function to get the data.
<!-- Use the action attribute to call the 'main' function --><form action="{{ url_for('main') }}" method="POST"><fieldset>     <legend>Input values:</legend>     Temperature:     <input name="temperature" type="number" required>     <br>     <br> Humidity:     <input name="humidity" type="number" required>     <br>     <br> Windspeed:     <input name="windspeed" type="number" required>     <br>     <br>     <input type="submit"></fieldset></form>
The above form, with the given CSS, gives us the following input fields and submit button:
You might have noticed the {{ }} brackets above. Whenever you see these, you should immediately think: that dude is using Jinja. Flask supports Jinja, a templating language, which uses the {% %} and {{}} syntax to include placeholder blocks or other chunks of code. In this case, I wrote “{{ url_for(‘main’) }}” to trigger the ‘main’ function in the app.py file.
Next up, I use an if condition and a for loop to display the prediction. The block is only showed or created if the condition is true. Remember that we stored the user input in a dictionary ‘original_input’ in the app.py file. Therefore, I iterate through the dictionary to print the user input, and last but not least, I display the prediction.
<!-- Our 'result' is false until a prediction has been made -->{% if result %}     <!-- Loop through dictionary and print key:value pair -->     {% for variable in original_input %}         <b>{{ variable }}:</b> {{ original_input[variable] }}     {% endfor %}     <br>     <!-- Print prediction -->     <br> Predicted number of bikes in use:     <p style="font-size:50px">{{ result }}</p>{% endif %}
The complete code is displayed below:
Let’s celebrate your work so far by running our flask web application. You will need to install all libraries in your virtual environment (use either pip or conda to install pandas, xgboost, scikit-learn, and pickle-mixin). Afterward type:
flask run
This will run your application on a local address like http://127.0.0.1:5000/. By visiting the URL, you should be met by the index.html file. You can stop the server by typing ctrl+c. Notice that the command triggers the creation of a ‘__pycache__’ file. It’s the compiled version of our app.py.
In order to make the application available for others, we still need a couple of minor steps. Let’s start by signing you up for a free account at GIT and Heroku. In order to interact with the two, we need to install GIT and the Heroku command-line interface known as Heroku-CLI. If you are using VSCode, then I would recommend installing the Heroku and Heroku-CLI extensions.
Although flask is wonderful, it’s mainly used for local development. It isn’t designed to handle the requests that a normal web server receives. In order to handle a larger amount of requests, we need to install the gunicorn python library.
pip install gunicorn
Having installed gunicorn, we now need to tell Heroku to use it. We do that by creating a file called procfile without file extension (for example, Procfile.txt is not valid.). The file will specify which commands to execute on startup.
Enter this line inside the empty Procfile file:
web: gunicorn app:app
The first ‘app’ represents the name of the python file that runs your application or the name of the module it is in (i.e., if you had an application called run.py, then it would be run:app).
The second ‘app’ represents your app name (i.e. app = Flask(__name__)).
Heroku needs to know which libraries to install to run your application. We can automate this process by running:
pip freeze > requirements.txt
This will generate a txt file called requirements.txt with the libraries used in your application. Although brilliant, it does not include all the necessary packages. The list below shows the content of my requirements file (notice that I have included scikit-learn since XGBClassifier object directly interacts with scikit-learn API):
click==8.0.1Flask==2.0.1gunicorn==20.1.0itsdangerous==2.0.1Jinja2==3.0.1MarkupSafe==2.0.1Werkzeug==2.0.1pandasxgboostscikit-learnpickle-mixin
I did not add specific versions of all my libraries but feel free to do that.
Although not necessary, you can add a runtime.txt file if you wish to specify a specific python version. You can check which python version you're running by typing:
python -V
My runtime.txt file has the following:
python-3.9.6
This is the final stage before deployment, and the MyApp directory should look similar to this:
If all went well, you should start interacting with your Heroku account through the command line. Open your command line while you are inside the project folder and type:
heroku login
Use the email address and password used when creating your Heroku account. The command will open a browser window, but if you prefer to stay in the command line, just type:
heroku login -i
An example of a successful login:
##########################################
A login might not be successful if you are in the wrong environment. In this scenario, the following command might be useful: conda deactivate
##########################################
Before using the git commands to send our files to Heroku, you need to tell git who you are. This is important because every Git commit uses this information.
To do that, type this in the command line:
git config --global user.name "John Doe"
Press enter and then type:
git config --global user.email johndoe@example.com
Make sure to replace your email address and your name appropriately in those lines keeping the double-quotes. You need to do this only once if you pass the — global option.
Create a local git repository by typing:
git init
Add all your local files to the online repository by typing:
git add .
Make sure to include the dot after add. The dot means you are adding the entire directory to the repository.
Commit your files with:
git commit -m "First commit"
The next steps consist of creating an empty app on Heroku and sending our local files to that app. And we will be sending the files using git.
Create an empty Heroku app:
heroku create <app-name-you-choose>
Once the app has been created, you can enter your Heroku account to get an overview:
The final steps consist of taking all our files, pushing them to Heroku, and then activating the app:
git push heroku master
Activate the app and set the number of dynos (anything more than one will cost):
heroku ps:scale web=1
If all went well, then you should be able to access your app at:
<app-name-you-choose>.herokuapp.com
You can open it by simply following the URL above or typing:
heroku open
It might take some time to function, so have patience. If all went well, you should have an app running on the web. You should be proud of yourself.
If you feel comfortable or want to see a different variation of the deployment, I can recommend visiting my advanced deployment. The files are made available on my git.
The following commands have been great dealing with all the errors I saw along the way:
heroku local Use this to open Heroku on a local server. Remember to install all required libraries.
heroku logout It should be self-explanatory.
heroku run 'ls -al' A copy of your dyno to have it list the directory contents. This is useful when you add more files and need to check that they have been added on Heroku.
heroku logs --tail With this Heroku command, you can access information about your app’s performance once it’s up and running.
heroku restart It should be self-explanatory.
heroku apps:rename <newname> Rename your app.
I hope you enjoyed this article as much as I have enjoyed writing it. Leave a comment if you have any difficulties understanding my code. The Data Science community has given me a lot, so I am always open to giving back.
Feel free to connect with me on Linkedin and follow me on Medium to receive more articles.