Run a Flask application via uWSGI and Nginx


uWSGI can be used to serve a web application developed with the Python framework Flask. This technology is also in use for my own blog. There are several ways how to setup uWSGI, but this blog post will cover how to manage it via systemd. One part covers the configuration of the web server Nginx to forward all requests to the Flask web application. The blog post also includes how to configure SSL and getting a trusted certificate via Let's Encrypt. All commands which require root privileges will start with the command sudo. If you use the root user to install all the packages, you can just ignore this command and enter the parts right after sudo. All the steps were tested with the latest stable version of Debian (stretch). So if some commands fail, maybe you use a different operating system or a different version of Debian. In this case feel free to contact me and I will try to help you.

Setup uWSGI

First of all you have to install the corresponding Debian packages uwsgi and uwsgi-plugin-python, via the following command:

sudo apt-get install uwsgi uwsgi-plugin-python3

The next step is to create an appropriate systemd template unit file. An advantage of this setup is that every web application will run under its own user. Open the following file with your preferred editor, for example the editor vi:

sudo vi /etc/systemd/system/uwsgi-app@.socket

Add the following content to this new file:

[Unit]
Description=Socket for uWSGI app %i

[Socket]
ListenStream=/var/run/uwsgi/%i.socket
SocketUser=www-%i
SocketGroup=www-data
SocketMode=0660

[Install]
WantedBy=sockets.target

Now create a new template for an uWSGI service:

sudo vi /etc/systemd/system/uwsgi-app@.service

Copy the following content into this new file

[Unit]
Description=%i uWSGI app
After=syslog.target

[Service]
ExecStart=/usr/bin/uwsgi \
        --ini /etc/uwsgi/apps-available/%i.ini \
        --socket /var/run/uwsgi/%i.socket
User=www-%i
Group=www-data
Restart=on-failure
KillSignal=SIGQUIT
Type=notify
StandardError=syslog
NotifyAccess=all

For each web application a separate user should be created for security reasons. Use the following command to add a new user:

sudo adduser www-blog --disabled-login --disabled-password \
  --ingroup www-data --home /var/www/blog --shell /bin/false

The following part explains each option of the previous command. The description originates from Debian 7.7 - man page for adduser (debian section 8).

  • --disabled-login
    • Do not run passwd to set the password. The user won't be able to use her account until the password is set.
  • --disabled-password
    • Like --disabled-login, but logins are still possible (for example using SSH RSA keys) but not using password authentication.
  • --ingroup
    • Add the new user to the specified group instead of a usergroup or the default group
  • --home
    • Use the specified directory as the user's home directory, rather than the default specified by the configuration file.
  • --shell
    • Use specified shell as the user's login shell, rather than the default specified by the configuration file.

The following two commands are required to start the service and socket each time the systems boots.

sudo systemctl enable uwsgi-app@blog.socket
sudo systemctl enable uwsgi-app@blog.service

Now the configuration file for the web application can be configured. Due to the fact that the directory /etc/uwsgi/apps-available/ was chosen during the configuration of the templates, a file within this directory must be created. For example /etc/uwsgi/apps-available/blog.ini.

[uwsgi]
# -------------
# Settings:
# key = value
# Comments >> #
# -------------

# Base application directory
# chdir = /full/path
chdir  = /var/www/blog

# WSGI module and callable
# module = [wsgi_module_name]:[application_callable_name]
module = secblog
callable = app 

# Virtualenv
venv = /var/www/blog/venv

# Plugins
plugin = python3

# master = [master process (true of false)]
master = true

# processes = [number of processes]
processes = 5

uid = www-blog
guid = www-blog

Via the parameter module the Python package of the web application must be specified. The parameter callable specifies the Flask object. The following snippet shows an example application, to describe which values should be defined for module and callable. A virtualenv can also be used. The folder of this virtualenv can be specified via the parameter venv.

The working directory in this example is /var/www/blog. Within the directory, the Python package secblog is used. The packages typically include an init file (__init__.py). Within this init file, the Flask application is initialized and in this example referenced by the variable app.

 |_ blog
    |_ __init__.py

Example content of the file __init__.py

from flask import Flask
from config import settings

app = Flask(__name__)
app.config.from_object('config.settings')

The last step of the uWSGI setup is to start the socket of the web application via the following command

sudo systemctl start uwsgi-app@blog.socket

These are the required steps to configure the uWSGI service. The next part covers the setup of the web server, in this case Nginx.

Setup Nginx including HTTPS support

First of all you have to install the required Debian packages via the following command. For the HTTPS support, a free certificate provided by Let's Encrypt will be used. Hence, the package certbot must be installed too. If you use an older version of Debian than 9 (stretch), you should have a look at the homepage of certbot.

sudo apt-get install nginx certbot

The next step is to create a new certificate. Since the Python application does not have a common web root, the default page of Nginx should be used to create the Let's Encrypt certificate. So to use this default page, you simply have to enter the following command:

sudo ln -s /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
sudo systemctl restart nginx

The default page will listen by default for every hostname. So if the DNS entry, for which you want to create an SSL certificate, points to this webserver, the default page will handle the requests. Now the command to create a certificate can be entered:

sudo certbot certonly --webroot -w /var/www/html -d example.com 

The meaning of the specified parameters are the following:

  • certonly
    • Create only the certificate
  • --webroot
    • For the verification process, a file will be placed in the webroot folder
  • -d
    • Defines the hostname, the certificate should be issued for
  • -w
    • Specifies where the webroot is located on the local storage
    • Default one for the chosen Nginx page should be /var/www/html

For any additional information check out the website of certbot Deploying Let's Encrypt certificates: Nginx on Debian 9 (stretch).

The next part covers the details about how to configure the Nginx webserver. Since Nginx version 0.8.40 native support for passing traffic to a Python application via the uWSGI protocol is included. So no additional modules are required or nothing else has to be enabled.

First of all you have to configure the site page for the new blog. In Nginx all site pages are located in the folder /etc/nginx/sites-available/.

An example site page can look like the following. The first part covers how Nginx should handle unencrypted HTTP requests. In this case all requests will be redirected to the encrypted version of this blog. The parts starting with a comment are for the renewal of the Let's Encrypt certificate. Because every 90 days the certificate expires and an option is to allow access to the folder /var/www/html. So certbot can create a file within this directory and check if this file is reachable via the Internet. Without these lines the renew process of the certificate will fail. After you retrieved a new certificate you can add the comments.

The part bettercrypto ssl stuff is about hardening the SSL settings. These settings may change in the future, so I recommend to take a look at the page of BetterCrypto.org, especially the Applied Crypto Hardening PDF document they provide for SSL hardening.

The last part of the configuration covers the actual uWSGI setup. It's important to add the right path of the uWSGI socket. If the configuration is finished the requests should all be forwarded to the Python application via the uWSGI socket.

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
    # For renewal of the Let's Encrypt certificate only
    # root /var/www/html;
    # location /.well-known/ {
    #    root /var/www/html;
    # }
}

server {
    listen 443 ssl;
    server_name example.com www.example.com;
    # 
    ssl_certificate /etc/letsencrypt/live/www.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.example.com/privkey.pem;
    # bettercrypto ssl stuff
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/ssl/private/dhparams.pem;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # not possible to do exclusive
    ssl_ciphers 'EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA';

    add_header Strict-Transport-Security "max-age=155520000; includeSubDomains; preload;";
    add_header X-Frame-Options SAMEORIGIN;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "no-referrer";

    # bettercrypto ssl stuff end

    # set max upload size
    client_max_body_size 10G;
    fastcgi_buffers 64 4K;

    # Disable gzip to avoid the removal of the ETag header
    gzip off;

    # Uncomment if your server is build with the ngx_pagespeed module
    # This module is currently not supported.
    #pagespeed off;

    access_log /var/log/nginx/blog_access.log;
    error_log /var/log/nginx/blog_error.log;

    # redirect requests to the uwsgi socket
    location / { try_files $uri @blog; }
    location @blog {
        include uwsgi_params;
        uwsgi_pass unix:/var/run/uwsgi/blog.socket;
    }

}

Now this new page can be enabled via the following command. In this example I assume the new page got named blog. So the full path to this configuration file is /etc/nginx/sites-available/blog.

sudo ln -s /etc/nginx/sites-available/blog /etc/nginx/sites-enabled/blog
sudo systemctl restart nginx

References

This section covers all the references mentioned in the blog post:

[Update 07-05-2018]: Fixed a typo -> /etc/nginx/sites-enable(d)/blog