Web applications are used to drive business like banking, social media etc and an important aspect of each web app is how many users are using it. The user traffic determines popularity of the web app and corresponding revenue generation from it. To have good user traffic the app needs to perform well as the load of users increase.
Handling user traffic should be a part of app design and then resources to manage burst in user traffic should be intelligently available to the app when needed. All these aspects of an app being available over internet falls under deployment. Deploying a flask app means setting it on a server so that it can be accessed over internet.
Understanding the Deployment Landscape
In the early days of web development, around 1995, most servers stored static content like files and served them to the client. These servers were well-equipped to cache files in memory, reducing the need to access the hard disk.
As websites began to feature dynamic content, servers needed to serve HTML pages with interactive elements generated on the server-side. This was made possible by storing scripts on the server. These scripts would act upon each request, generating content on demand. However, one challenge was that every new request required rerunning the script. A more efficient setup enabled each request to call a function instead. This function would route the request to the web application and return the response. Running a function was much more resource-efficient than rerunning an entire script.
These functions are what constitute current web applications written in Flask. The service responsible for calling the web application as a function is the WSGI (Web Server Gateway Interface). When deploying, you typically have three components: a load balancer, such as NGINX, a WSGI request handler, like gUnicorn or uWSGI, and a Flask application. The load balancer routes requests to different request handlers and each request handler passes on the request to a Flask application. Thus, the infrastructure is as follows.
Flask Application Setup
Now that we have a basic grasp of how the external world interacts with a Flask application, let's explore how to tailor a Flask application for production. We will assume a Flask application is made up of Models, Views, and Controllers.
A model represents how data is saved by the application. Normally, it relies on a database. For a production-ready database, availability, security, and monitoring should be considered.
A view represents how the user sees the application. It varies from application to application. Production-ready views should incorporate frontend best practices such as minification of files and efficient bundling of frontend resources.
The controller hosts the application logic. It can carry out a variety of tasks, like handling long-running IO tasks, quick burst CPU intensive tasks, asynchronous tasks, error handling, etc. All these tasks have production aspects such as proper monitoring/logging using tools like Grafana, managing long-running tasks using queues to offload execution efforts, and more.
Deployment Best Practices
Here are some guidelines to manage your app in production effectively. Keep in mind that the need for an application may vary. For instance, a Flask App serving only API requests won't require a View layer. Or, one might use a cloud provider database that can offload a lot of maintenance work with proper configuration.
Flask applications often require consistent dependencies. To ensure this, add all package requirements into a text file using the command
pip freeze > requirements.txt. When installing dependencies for the app, use the commandpip install -r requirements.txt.The app may require different variables to run in different environments. These variables should be passed as environment variables to the app, safely loaded into memory. Do not store them on a file and expose them in a repository as a security vulnerability.
Example Flask application
We'll create a simple Flask application and focus solely on its deployment to understand the process. The project structure is as follows (Note: we will use docker to manage our deployment).
As you can see, there are two main folders: the app and nginx. The app represents the flask application, it has the server code and gunicorn settings which will help it run as a production server. The nginx folder is where we have configured a load balancer to route request and serve static files. Lets deep dive into them one by one. (Just a note here, we are installing two packages flask and gunicorn via the requirements.txt to run the entire application.)
Inside the app folder, the app/app.py is a flask server with the following code. For all requests to root URL the server responds with current timestamp, and variables MSG and DB_CONNECTION_STRING from config. The config is something which is chosen by the environment in which we run the code, and in real world variables like MSG and DB_CONNECTION_STRING denote environment specific settings for different supporting components like database, message queues, search databases etc.
from flask import Flask
from datetime import datetime
import config
app = Flask(__name__)
@app.route('/')
def index():
return f"""
MSG: {config.MSG}<br/>
DB STRING: {config.DB_CONNECTION_STRING}<br/>
Time: {datetime.now().isoformat()}""The magic of config is supported by two files, lets look at app/config/settings.py .
class BaseConfig():
DB_CONNECTION_STRING = "BASE DB STRING"
MSG = "Inside BASE CONFIG"
class DevConfig(BaseConfig):
DB_CONNECTION_STRING = "DEV DB STRING"
MSG = "Inside DEV CONFIG"
class ProductionConfig(BaseConfig):
DB_CONNECTION_STRING = "PRODUCTION DB STRING"
MSG = "Inside PRODUCTION CONFIG"This file is just having setting specified for different environments (DEV and PRODUCTION) here. In real world you can extend this for different setting options for different environments, for example, you may not want to allow debugging in production applications. Next we see the app/config/__init__.py
import os
import sys
import config.settings
APP_ENV = os.environ.get('APP_ENV', 'Dev')
_current = getattr(sys.modules['config.settings'], '{0}Config'.format(APP_ENV))()
for atr in [f for f in dir(_current) if not '__' in f]:
val = os.environ.get(atr, getattr(_current, atr))
setattr(sys.modules[__name__], atr, val)This file is tricky to understand so lets detail it out. The APP_ENV is an environment variable that can be Dev or Production. Since we import config.settings this becomes available in sys.modules and our config classes (like DevConfig) are dynamically loaded using the getattr function. Next whatever are our config class attributes like DB_CONNECTION_STRING, MSG are dynamically attached to the config object using the setattr function and now referring to config.MSG dynamically refers to DevConfig.MSG or ProductionConfig.MSG depending on the APP_ENV variable. This is one of the ways to have environment specific behavior injected in the application. Next we will see the app/gunicorn_config.py which is a config file to run gunicorn server.
import os
workers = int(os.environ.get('GUNICORN_PROCESSES', '2'))
threads = int(os.environ.get('GUNICORN_THREADS', '4'))
bind = os.environ.get('GUNICORN_BIND', '0.0.0.0:8080')
forwarded_allow_ips = '*'
secure_scheme_headers = {'X-Forwarded-Proto': 'https'}Most of the content in this file is to parametrize gunicorn run with default values. Next we will keep a static file secret.txt inside /app/project/static. This has the content : "The secret code is 5134" and will be displayed using nginx. Finally we will understand the /app/Dockerfile.
FROM python:3.7.3-slim
COPY requirements.txt /
RUN pip3 install --upgrade pip
RUN pip3 install -r /requirements.txt
COPY . /app
WORKDIR /app
CMD ["gunicorn","--config", "gunicorn_config.py", "app:app"]We use a base image of Python 3.7 and copy our requirements.txt to the docker container. Then install the needed packages by pip install and copy the content of /app_deploy to /app in docker container. Finally we run the container using the gunicorn command specified as CMD variable. This is all what we need to do to create the flask server container that we want.
Next we see the nginx container that will act as a load balancer and proxy to serve static content. For this we will manually create a nginx.conf file /app_deploy/nginx/nginx.conf with the following content.
upstream hello_flask {
server web:8080;
}
server {
listen 80;
location / {
proxy_pass http://hello_flask;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static/ {
alias /app/project/static/;
}
}This is mostly configuration, but important is to see the location /static/ { .. setting, here we provide the location of static files in the flask server to nginx. The /app_deploy/nginx/Dockerfile looks as follows, it is used to create a nginx container that will act as a load balancer to the flask server.
FROM nginx:1.25
RUN rm /etc/nginx/conf.d/default.conf
COPY nginx.conf /etc/nginx/nginx.conf.dNow so far we have created two docker containers using Dockerfile, but these two need to interact and work together. Plus running multiple docker containers is best accomplished by using docker-compose. All the docker operational knowledge can be obtained from the pre requisite section. Here is our /app_deploy/docker-compose.yml.
services:
web:
build: ./app
ports:
- 8085:8080
environment:
- APP_ENV=Dev
nginx:
build: ./nginx
ports:
- 1337:80
depends_on:
- webThis config specifies two services web and nginx. The web service looks for a Dockerfile in the /app_deploy/app directory, which is our flask server Dockerfile. It creates a container and handles port mapping as docker container port 8080 maps to host machine port 8085. We also provide environment variable APP_ENV as Dev, to trigger DevConfig settings to show environment based behavior change. Similarly the nginx web service looks for Dockerfile in /app_deploy/nginx directory and creates the nginx container. Just this container has a pre-requisite of creation of the web service as static file mapping has to work via nginx.
Now what we have created, we can check by turning on the containers by command docker-compose up. This runs both the containers web and nginx and if we go to http://localhost:8085/ we see the following.
And if we just change the environment variable in docker-compose.yaml to Production.
services:
web:
build: ./app
ports:
- 8085:8080
environment:
- APP_ENV=ProductionThe output changes to
And with this setup, if we look for static file by visiting http://localhost:8085/static/secret.txt we see the following output.
Note, we don't have a dedicated endpoint to serve static flask from yet we can see the static content and this is because of the nginx container we have configured.
Conclusion
In this topic, we ventured into the reasons for needing a WSGI app running ahead of a Flask app. We examined some of the best approaches for deploying a Flask app, looked at creating a Flask app and controlling it via environment variables, placed the Flask server behind a WSGI gunicorn server, and added an nginx load balancer in front of the flask container to load balance and serve static files. Although this is just an example of a Flask app deployment, it should be noted that a real-world Flask app will have multiple configuration settings and dependencies for components like message queues, databases, etc. Breaking down the app into Docker container units assists in scaling different components as needed. Finally, if you have a dockerized Flask app, you can choose any cloud vendor, like AWS, GCP, Heroku, or Azure, to host this app in the cloud and manage the dependencies effectively.