How to properly host Flask application with Nginx and Guincorn

How to host Flask with Nginx and Gunicorn

I recently need to host a Flask web application, and decided to share my experience with this comprehensive guide on how to properly host a Flask web application with Nginx http server and Gunicorn WSGI server.

Install Nginx

Nginx is an open-source HTTP server and reverse proxy, as well as an IMAP/POP3 proxy server. Nginx is known for its high performance, stability, rich feature set, simple configuration, and low resource consumption. We will use Nginx to handle all the http communication, and serving the static content, as well as a reverse proxy server that passes the dynamic content requests to the backend server.

As usual, I’m using a Raspberry Pi as my Linux machine running Raspbian Stretch OS. This is equivalent to any Debian based self-host or VPS Linux machine. I’m using the default user name Pi which is pre-configured by Raspbian throughout this article, however, for security reason, it is advisable to tighten the security of your Linux machine and do not use this default user or root user for your server configuration.

It easy to install Nginx with a single command:

sudo apt-get update
sudo apt-get install nginx

and the Nginx will start automatically. If for some reason it is not, start the server with:

sudo service nginx start

By default, Ngnix creates a web server root directory at /var/www/html/ with a default html web page in the directory. When nginx is up and running, the webpage will be visible when pointing the browser url to your Linux machine.

nginx-up-and-running
Nginx is up and running

Install Flask Application and Dependencies

With the Nginx up and running, we can now upload the Flask application to the /var/www/html/ directory. Your Linux machine probably need to install some python libraries (i.e. the dependencies) in order for the Flask application to function properly. In my case, Raspberry Pi by default has Python 3.5.x pre-installed, however python installation tool pip is not available, and certainly no Flask package yet.

sudo apt-get install python3-pip
sudo pip3 install flask

I install Flask and running Flask globally without using a virtualenv. This is just a personal preference on setting up web server. Feel free to install virtualenv if required.

If you run your Flask application through local connection at 127.0.0.1:5000 (or localhost:5000) during your development, you may want to make minor change on the Flask application to set the host '0.0.0.0' to have the server available externally so that we can test it.

if __name__ == '__main__':
    app.run(host="0.0.0.0")

Launch the Flask application to ensure it works as it was on your development platform, remember to change the filename myapp.py to match the flask app filename that you are using:

cd /var/www/html
python3 myapp.py
 * Serving Flask app "myapp" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Launch browser and point the url to the IP address and port 5000 of the Linux machine (i.e. 192.168.0.101:5000) to make sure that the Flask application works.

Press CTRL C on the Linux machine to terminate the flask application.

So far you are able to view the Flask web page by utilising Flask built-in web server, as you’ve probably seen the warning message every time you run the Flask script, while lightweight and easy to use, Flask’s built-in server should only be used for development purpose as it doesn’t scale well. We need to setup the Nginx as http server and reverse proxy server, and then deploy a production WSGI server for properly running Flask in production.

Setup Nginx

Although the Nginx works out of box with the default pre-configured settings, however, it can be optimised for better performance by altering the settings of /etc/nginx/nginx.conf:

sudo nano /etc/nginx/nginx.conf

Make the following changes for improving potential performance:
1) Uncommented the multi_accept directive and set to on, which informs each worker_process to accept all new connections at a time, opposed to accepting one new connection at a time.

2) Change the keepalive_timeout from default 65 to 30. The keepalive_timeout determines how many seconds a connection to the client should be kept open before it’s closed by Nginx. This directive should be lowered so that idle connections can be closed earlier at 30 seconds instead of 65 seconds.

3) Uncomment the server_tokens directive and ensure it is set to off. This will disable emitting the Nginx version number in error messages and response headers.

4) Uncomment the gzip_vary on, this tell proxies to cache both the gzipped and regular version of a resource where a non-gzip capable client would not display gibberish due to the gzipped files.

5) Uncomment the gzip_proxied directive and set it to any, which will ensure all proxied request responses are gzipped.

6) Uncomment the gzip_comp_level and change the value to 5. Level provide approximate 75% reduction in any ASCII type of files to achieve almost same result as level 9 but not have significant impact on CPU usage as level 9.

7) Uncomment gzip_http_version 1.1;, this will enable compression both for HTTP/1.0 and HTTP/1.1.

8) Add a line gzip_min_length 256; right before gzip_types directive, this will ensure that the file smiler than 256 bytes would not be gzipped, the default value was set at 20 bytes which is too small and could cause the gzipped file even bigger due to the overhead.

9) Replace the gzip_types directive with the following more comprehensive list of MIME types. This will ensure that JavaScript, CSS and even SVG file types are gzipped in addition to the HTML file type.

    gzip_types
       application/atom+xml 
       application/javascript 
       application/json 
       application/rss+xml 
       application/vnd.ms-fontobject 
       application/x-font-ttf 
       application/x-web-app-manifest+json 
       application/xhtml+xml 
       application/xml 
       font/opentype 
       image/svg+xml 
       image/x-icon 
       text/css 
       text/plain 
       text/x-component 
       text/javascript 
       text/xml;

So far we have configure the event and http directive blocks through /etc/nginx/nginx.conf file, the server directive is usually stored in a separate configuration file and get loaded into the overall configuration structure through include /etc/nginx/sites-enabled/*; directive at the end of /etc/nginx/nginx.conf file. The actual server configuration file for each server is usually stored in /etc/nginx/sites-availabledirectory, and linked by a symbolic link in /etc/nginx/sites-enabled directory, this allows you to keep multiple server configuration files in sites-available directory and only activate a particular server by adding a symbolic link at sites-enabled directory.

Before we starting to configure the server directive, let’s first understand a little bit of the files and directory structure of a Flask application web site. The understanding of the files and directory structure would help us to configure the web server properly for high-performance.

pi@raspberrypi:/var/www/html $ ls -l
total 44
-rw-r--r-- 1 pi pi       1008 Aug 21 19:19 e-tinkers_icon.png
-rw-r--r-- 1 pi pi       6685 Aug 22 13:54 myapp.py
drwxr-xr-x 2 pi pi       4096 Aug 21 14:30 posts
drwxr-xr-x 2 pi www-data 4096 Aug 22 13:56 __pycache__
-rw-r--r-- 1 pi pi         79 Aug 21 21:34 robots.txt
-rw-r--r-- 1 pi pi        554 Aug 21 19:18 sitemap.xml
drwxr-xr-x 6 pi pi       4096 Aug 21 22:45 static
drwxr-xr-x 2 pi pi       4096 Aug 21 19:22 templates
pi@raspberrypi:/var/www/html $ 

Currently we have /var/www/html as our website’s root directory, within it a typical Flask application would consists one or more .py python script files and directories (such as all the class and function definitions) that you created, like the posts/ directory in this Flask app. Flask conventionally stores static contents such as css, JavaScripts, fonts and images in statics/ directory, Flask also conventionally stores template files in template/ directory, those html files within the template/ directory are not static html per se, they are template files that need further process by Flask file before it can be rendering as html pages. The python interpreter also generated a __pycache__/ directory. In addition to those files and directories, a web site would probably includes other files that are not going to be handled by the Flask app, but will be handled by http server, such as sitemap.xml, robots.txt and favicon.ico or site-logo.png, those files are typically used by search engines and browser and are typically located at document root directory of a web site.

We’d like our configuration to work in such a way where the Nginx server will handling all the statics contents (i.e. static/ and those files not related to Flask (i.e. sitemap.xml, robots.txt and icon), and only pass the rest to the Flask for further process.  With this understanding, let’s configure our server block.

First, create a file at /etc/nginx/sites-available/, give it a name reflect your server application purpose such as myflaskapp:

sudo nano /etc/nginx/sites-available/myflaskapp

Enter the following nginx directives and save the file:

server {
	listen 80 default_server;
	listen [::]:80;

	root /var/www/html;

	server_name example.com;

	location /static {
	    alias /var/www/html/static/;
	}

	location / {
	    try_files $uri @wsgi;
	}

	location @wsgi {
	    proxy_pass http://unix:/tmp/gunicorn.sock;
	    include proxy_params;
	}

	location ~* .(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|css|rss|atom|js|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
	    access_log off;
	    log_not_found off;
	    expires max;
	}
}

With this set of Nginx configurations, we told Nginx to listen on port 80 for HTTP requests directed at the domain example.com. For every such request, we tell Nginx to reference the directory /var/www/html as the document root.

The location /static map the request to the file inside the /var/www/html/static/ directory and serve the static content directly without passing it to the Flask app.

The location / block map request to a file inside the document root directory and return it as a HTTP response if there is one. This ensure the content such as robots.txt get served by the Nginx server. If the request is not a file that can be found at this root directory, Nginx should issue an internal redirect to the upstream server defined in @wsgi block.

The location @wsgi block proxies HTTP requests to the /tmp/gunicorn.sock unix socket where Flask app will listen to, plus some common HTTP headers defined inside the /etc/nginx/proxy_params file.

The last location block tells the Nginx to turn off the access log and set the cache expiry date to maximum for those specific static content types.

By default, Nginx enable the server configuration located in /etc/nginx/sites-available/default by creating a symbolic link at /etc/nginx/sites-enabled/, we need to disable the symbolic link linked to the default configuration and create a link to enable our new server configuration.

cd /etc/nginx/sites-enabled/
sudo rm default
sudo ln -s /etc/nginx/sites-available/myflaskapp .

Test the Nginx configuration with command:

sudo nginx -t

If everything looks ok, go ahead and reload the Nginx configuration:

sudo service nginx reload

We have completed the setup of Nginx server, let’s move on to setup the upstream Flask application server.

Web Server Gateway Interface (WSGI)

In order for various python frameworks and applications to be able to communicate with different type of HTTP servers, Python community developed the WSGI specification for standardising the interface of communication between an application or framework and a HTTP web server.

There are several popular WSGI servers available in the market. These servers provide the interface between an HTTP server and application/framework. HTTP requests are received by a HTTP server such as Nginx and passed along to WSGI-compliant framework/application such as Flask application.

  • Gunicorn is a stand-alone WSGI web application server which offers a lot of functionality. It natively supports various frameworks with its adapters, making it an extremely easy to use drop-in replacement for many development servers that are used during development.
  • uWSGI itself is a vast project with many components “aiming to provide a full stack for building hosting services”. One of these components, the uWSGI server, runs Python WSGI applications. It is capable of using various protocols, including its own uwsgi wire protocol, which is similar to SCGI. Nginx web server supports uWSGI’s uwsgi protocol natively.
  • mod_wsgi is an Apache WSGI-compliant module allows you to run Python WSGI applications on Apache HTTP Server. It can be configured either by embedding the code and executing it within the child process; or as a daemon whereby the WSGI application has its own distinct process, managed automatically by mod_wsgi.

Which WSGI server to use is a personal choice with various considerations. Personally, I like Gunicorn for its easy to configure, well-written documentation and its native support of Flask framework.

Setup Gunicorn

Gunicorn is a Python WSGI HTTP Server for UNIX, and is therefore easy to install using pip:

sudo pip3 install gunicorn

To run the Gunicorn manually:

gunicorn --bind=unix:/tmp/gunicorn.sock --workers=4 myapp:app

The –bind=unix:/tmp/gunicorn.sock bind the unix socket to what we setup on Nginx location @wsgi configuration.
The –workers=4 specify the number of worker processes for handling requests. Gunicorn recommends to set this number to 2 to 4 times of the number of CPU cores of your Linux machine. You may want to vary this a bit to find the best for your particular application’s work load, but for a new website without a much traffic, I simply use 4 to match the number of cores of my machine and adjust in future.

The last setting parameter myapp:app is in the form of module:callable. The module refer to the python script file (e.g. myapp.py) to run your Flask app, the callable needs to match the name of the Flask object that you instantiated in your Flask application app = Flask(__name__)(e.g. app is the name of the callable Flask object in this case).

Please refer to Gunicorn documentation for more settings, but many of those settings are either specified/set by Nginx (such as user, keepalive, etc.) or can be set later via System and Service Manager systemctl configuration.

One setting that worths mentioned and I didn’t use in my Gunicorn setting is --threads as it is equally important as --workers, especially if you are renting a basic VPS droplet which often has a single core CPU. Whether to use --workers or --threads or a combination of both depend on traffic, the type of contents you have and your Linux machine configuration, and requires some test in real environment.

Launch your browser and point to the Linux server’s IP address (e.g. 192.168.0.101), if you see your Flask web application running, it means that both Nginx and Gunicorn have been setup correctly (since it serves at port 80, instead of the port 5000 via Flask internal HTTP server).

Press CTRL C to stop the Gunicorn.

So far we run Gunicorn manually, we’d want it to run automatically when the server is up and running.

Create a Gunicorn Systemd Service File

There are many ways to run a user service upon boot, but since the majority of Linux distributions have adopted “System and Service Manager” systemd as an init system used to bootstrap the user space and to manage system processes after booting, We will use systemd to automate the running of Gunicorn.

We need to create a gunicorn.service configuration file:

sudo nano /etc/systemd/system/gunicorn.service

The configuration file looks like this:

[Unit]
Description=gunicorn daemon for /var/www/html/myapp.py
After=network.target

[Service]
User=pi
Group=www-data
RuntimeDirectory=gunicorn
WorkingDirectory=/var/www/html/
ExecStart=/usr/local/bin/gunicorn --bind=unix:/tmp/gunicorn.sock --workers=4 myapp:app
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID

[Install]
WantedBy=multi-user.target

The Description is just a string for describing the service, change accordingly to reflect your application and purpose.

The User is the Linux user name you use so far to run your python script.

The ExecStar matches the command we previously used to run the Gunicorn manually. Please make sure the absolute directory of your Gunicorn installation. If you are not sure, run which gunicorn on your terminal to find out the directory.

We can now start the Gunicorn service we created and enable it so that it starts at boot:

sudo systemctl enable gunicorn
sudo systemctl start gunicorn

To check if Gunicorn service process running properly:

sudo systemctl status gunicorn

If you make changes to the /etc/systemd/system/gunicorn.service file, reload the daemon service definition and restart the Gunicorn process by typing:

sudo systemctl daemon-reload
sudo systemctl restart gunicorn

What’s Next

We have setup the Nginx as HTTP server to serve static content and handle all the HTTP requests, as well as a proxy server to handle client requests for the Flask application. We’ve configured Gunicorn to translate client requests so that Flask can handle them. This article does not cover the setting up of SSL because my previous article on implementing SSL is valid for most of the Flask-based SSL implementation.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.