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 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 by pointing the browser url to your Linux machine.

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 (or localhost:5000) during your development, you may want to make minor change on the Flask application to set the host '' to have the server available externally so that we can test it.

if __name__ == '__main__':"")

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

cd /var/www/html
 * 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 (Press CTRL+C to quit)

Launch browser and point the url to the IP address and port 5000 of the Linux machine (i.e. 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.


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
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 usually 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;


	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 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, it also setup 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 it in future when there is a demand on traffic.

The last setting parameter myapp:app is in the form of module:callable. The module refer to the python script file (e.g. 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., 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 and manage the user space 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:

Description=gunicorn daemon for /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


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 ExecStart 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, the Nginx also serves 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.

29 comments by readers

  1. Hello

    the below line works:

    gunicorn --bind=unix:/tmp/gunicorn.sock --workers=4 start_backend:web_app

    but /etc/systemd/system/gunicorn.service

    is throwing error:

    gunicorn.service - gunicorn daemon for /var/www/html/
    Loaded: loaded (/etc/systemd/system/gunicorn.service; enabled; vendor preset: enabled)
    Active: failed (Result: exit-code) since Thu 2018-12-27 11:57:34 IST; 3s ago
    Process: 4530 ExecStop=/bin/kill -s TERM $MAINPID (code=exited, status=1/FAILURE)
    Process: 4528 ExecStart=/usr/local/bin/gunicorn --bind=unix:/tmp/gunicorn.sock --workers=4 start_backend
    Main PID: 4528 (code=exited, status=203/EXEC)
    Dec 27 11:57:33 raspberrypi systemd[1]: Started gunicorn daemon for /var/www/html/
    Dec 27 11:57:33 raspberrypi systemd[1]: gunicorn.service: Main process exited, code=exited, status=203/EXE
    Dec 27 11:57:34 raspberrypi systemd[1]: gunicorn.service: Control process exited, code=exited status=1
    Dec 27 11:57:34 raspberrypi systemd[1]: gunicorn.service: Unit entered failed state.
    Dec 27 11:57:34 raspberrypi systemd[1]: gunicorn.service: Failed with result 'exit-code'
    1. Services usually fail because of a missing dependency, missing configuration, or incorrect permissions. I would suggest that you check with your gunicorn.service configuration for any typo, or things mentioned here before you restart the service.

      Regarding the error message, noticed that you have a failed service PID4530 while you are trying to start another process 4528. You can manually clear out failed units with the sudo systemctl reset-failed command. This should do the trick. But in order to be sure, you can run the following commands to make sure all the services that no longer needed get stopped and disabled with the following commands:

      sudo systemctl stop gunicorn.service
      sudo systemctl disable gunicorn.service
  2. Thank you very much for these clear instructions. I learned a lot the explanations were very helpful. What does it take to open this server to external connecions, outside of the local network ?

  3. Hello

    thanks for this instruction.

    Can you help with this error.

    When i executed gunicorn the app works perfectly. When i executed flask, the app works perfectly, but when i executed nginx the app always shows 404

    1. Please check your nginx logs (at /var/log/nginx/) to see what exactly caused the 404? Also check your nginx config to see if there is any typo, or run sudo nginx -t to test your configuration.

  4. man! thank you so much this helped me to setup my web app I suffered a lot, I have a question how can I add another web app it just will be another site just (HTML/CSS/Jquery) then I will create another flask app but it will be an API

  5. Hi, while testing nginx I am always getting the error “nginx: [crit] pread() /etc/nginx/sites-enabled/cric_scorio” failed (21: Is a directory)”. Please help.

    1. It is hard to figure out what’s going on without knowing what inside the /etc/nginx/sites-enabled/ or what’s inside the crib_scorio. Generally, whatever in the /etc/nginx/sites-enabled/ should be just a symlink pointing to the nginx configuration file in

    1. Run sudo systemctl status gunicorn would give you better idea on what’s going on. You can manually clear out failed units with the sudo systemctl reset-failed command.

  6. For me putting the gunicorn.sock file in /tmp did not work.
    Moving it to the flask app directory made it!

  7. Thanks for the tutorial. Very helpful. I do have a question; I’m confused what the server_name is used for or what it’s purpose is?

  8. Ok. Thats what I thought I just wanted to verify. If I’m just hosting an internal website for small projects and just accessible via VPN, what elements of the nginx directives are necessary or critical?

    1. If it is an internal app and not much traffic, you probably don’t need the nginx and even gunicorn, just create a systemd or crontab to auto start the python script in the background python3

      1. I found Flask’s development web-server unreliable and hung quite often, especially if accessed from two different browsers at the same time. Maybe I’m doing something wrong. However, now that I’m running Flask on top of Nginix and Gunicorn I haven’t had any problems. Very stable. I need the stability because I’m building a Flask app to run my sprinklers with a web front end for defining zones and watering durations etc… I’m using SQLite for storing everything. I also have local temp and humidity readings for feeding the watering schedule.

        1. It is true that Flask built-in Werkzeug WSGI is for development and testing only, not for production deployment. Gunicorn is a better solution for WSGI. But personally I never have the problem as you described with Werkzeug. Glad to see that we share the similar interests in IoT, I recently setup a small hydroponic environment using STM32 measuring ec, ph, temp and controlling dosing pumps, with nginx, Flask and mySQL as the gateway hosting on this Raspberry Pi (serving both WordPress for this blog and some backend python apps).

  9. I have set up everything I think, with successful nginx and gunicorn service starting correctly, but I still don’t have my flask running apparently. My app is a CalDAV service tho not serving any html. Any ideas?

    my file and I use app == Flask(__name__) which is I think __main__ as callable.

    My web server still responds with the Nginx HTML page and nothing else (I didn’t change the static as I don’t have one).

    1. The default Nginx HTML page is served by the nginx is a correct behaviour, the reason being that when a request hit /(i.e. your, the try_files $uri match the request, and nginx automatically serves index.html directly instead of passing the request to the back end server @wsgi. What you should do is to delete the location / block, and change the location @swgi to location /, this way all the requests will pass to your back-end, and nginx will not do any front-end serving for your app.

      The idea of having the line:

      if __name__ == '__main__':


      is that the will only be executed when you run the app locally/directly, when the app is run by Gunicorn, the will not be executed but replaced by whatever setup code within the if __name__ == '__main__': block of Gunicorn. Your app will be imported as a module and be be called by Gunicorn when it is ready.

  10. Thanks for the tutorial. Of all the examples I looked at this was by far the easiest for me to understand.

  11. Thank you so much for this!
    I used your tutorial as a general template to build my first dash website. Super easy to follow.
    As a small contribution, if you want to use a virtual environment “venv”, the only thing you need to change is in the gunicorn.service file (and, of course, activate your venv before installing python packages!):
    ExecStart=/home/pi/venv/bin/gunicorn –pythonpath=/home/pi/venv/bin/python3 –bind=unix:/tmp/gunicorn.sock –workers=4 myapp:server

    Also, for very large webpages, you should bump up the client_max_body_size variable in nginx.conf to about 100M and restart nginx service.

  12. Any idea why I might be getting “Address already in use” ?

    ● gunicorn.service - gunicorn daemon for SmartGarden
       Loaded: loaded (/etc/systemd/system/gunicorn.service; enabled; vendor preset: enabled)
       Active: failed (Result: exit-code) since Mon 2021-09-06 06:14:07 BST; 1min 4s ago
      Process: 486 ExecStart=/usr/local/bin/gunicorn --bind=unix:/tmp/gunicorn.sock --workers=4 --pythonpath /home/pi/flaske...
     Main PID: 486 (code=exited, status=3)
    Sep 06 06:14:06 pidemo gunicorn[486]: OSError: [Errno 98] Address already in use
    1. It simply means that both your flask app and gunicorn(WSGI) are trying to use the same address and port. Looking at the log, you had a process 486 that already using the port. You can either stop it with sudo service gunicorn stop or command line kill 486 before you re-start your gunicorn systemd service. Please also make sure that you Flask app had the proper guard block setup for your code.

Comments are closed.