Building a solid Flask configuration for Production

Here’s a standard production recipe for deploying flask application or service, which many of us have used countless times. It assumes Linux, Docker, Nginx and Gunicorn as production environment. There’s nothing special in this, just all things in one place, so you can copy-paste desired parts and fix them to your needs.

Step 1: Create an app

Let’s first create a small demo Flask application in an empty directory.

mkdir flask-deploy
cd flask-deploy
# init GIT repo
git init
# install dependencies into new pipenv environment
pipenv install flask
# create a dir for static assets
mkdir static
# create test static file
echo "Hello World!" > static/hello-world.txt

Then add some code to app.py:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello, World!'

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

We’ll also need a Dockerfile to build our container on server. Put this code to Dockerfile in the project directory.

FROM python:3.7.2

RUN pip install pipenv

ADD . /flask-deploy

WORKDIR /flask-deploy

RUN pipenv install --system --skip-lock

RUN pip install gunicorn[gevent]

EXPOSE 5000

CMD gunicorn --worker-class gevent --workers 8 --bind 0.0.0.0:5000 app:app --max-requests 10000 --timeout 5 --keep-alive 5 --log-level info

The last line here runs gunicorn with gevent lightweight concurrency lib for parallel request processing.

The code is ready, we can put it on Github now.

git add *
git commit -a -m 'Initial commit'
git remote add origin git@github.com:your-name/flask-deploy.git
git push -u origin master

Step 2: Configure server

We’ll need Nginx and Docker running on server, plus Git to pull the code. Login to your server via SSH and use a package manager to install them.

sudo yum install -y docker nginx git

AWS tip: don’t forget to allow ports 80 and 443 for HTTP(S) traffic, and port 22 for SSH in network settings of your instance!

Next step is to configure nginx. Top-level nginx.conf configuration file often good as it is (still better check it!), so you only need to create a new configuration file for a site.

cd /etc/nginx/conf.d
sudo vim flask-deploy.conf

Here is a sample site configuration file for nginx with following features:

  1. SSL is configured. You should have valid certificates for your domain, e.g. a free Let’s Encrypt certificate.
  2. www.domain.com requests are redirected to domain.com
  3. HTTP requests are redirected to secure HTTPS port.
  4. Reverse proxy is configured to pass requests to Gunicorn (we’ll get to it shortly).
  5. Static files are served by Nginx from a folder.
server {
    listen        80;
    listen	 443;
    server_name  www.your-site.com;
    # check your certificate path!
    ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt;
    ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers HIGH:!aNULL:!MD5;
    # redirect to non-www domain
    return	 301 https://your-site.com$request_uri;
}

# HTTP to HTTPS redirection
server {
        listen 80;
        server_name your-site.com;
        return 301 https://your-site.com$request_uri;
}

server {
        listen 443 ssl;
        # check your certificate path!
        ssl_certificate /etc/nginx/ssl/your-site.com/fullchain.crt;
        ssl_certificate_key /etc/nginx/ssl/your-site.com/server.key;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_ciphers HIGH:!aNULL:!MD5;
        # affects the size of files user can upload with HTTP POST
        client_max_body_size 10M;
        server_name your-site.com;
        location / {
                include  /etc/nginx/mime.types;
                root /home/ec2-user/flask-deploy/static;
                # if static file not found - pass request to Flask
                try_files $uri @flask;
        }
	location @flask {
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';

                proxy_read_timeout 10;
                proxy_send_timeout 10;
                send_timeout 60;
                resolver_timeout 120;
                client_body_timeout 120;
                
                # set headers to pass request info to Flask
                proxy_set_header   Host $http_host;
                proxy_set_header   X-Forwarded-Proto $scheme;
                proxy_set_header   X-Forwarded-For $remote_addr;
                proxy_redirect     off;

                proxy_pass http://127.0.0.1:5000$uri;
        }
}

Reload the configuration with sudo nginx -s reload and it’s done!

Step 3: Deploy an app

The last step is to deploy a docker container with the app.

cd ~
git clone https://github.com/your-name/flask-deploy.git
git checkout master
docker build -t app:1
docker run -d --restart='always' --name app-1 app:1

What happens here? First, we’re cloning the repository with test app. Then building a Docker image from the code using Dockerfile we created earlier, and running a new container, which will restart on any failure.

Now it’s time to test it!

# test HTTP protocol, you should get a 301 response
curl your-site.com
# HTTPS request should return our Hello World message
curl https://your-site.com
# and nginx should correctly send test static file:
curl https://your-site.com/hello-world.txt

That’s it. We have a minimalistic production configuration of our app running on AWS instance. Hope it will help you to start quickly!

How to use RS256 tokens with Flask-JWT

As a follow-up of my previous post on JWT authentication in Flask, I want to discuss the implications of using RS256 algorithm for signing the tokens with Flask-JWT library. First of all, what’s the difference between RS256 and HS256 (a standard one) algorithms for JWT?

  • HS256 stands for HMAC with SHA-256. That’s an algorithm which encrypts and hashes the message (a JSON data in our case) at the same time using symmetrical secret key. The same key is used for encryption and decryption of the message.
  • RS256 is an RSA encryption plus SHA-256 hashing. RSA is an asymmetric encryption algorithm, which means it operates on a pair of keys – public and private. Private key is used to encrypt a token, and public one – to decipher it. You can share the public key freely without compromising authentication scheme.

In a simple case, there might be no need to use RS256. However, if you want to validate tokens on client for any reason, for example, to protect against MITM attack (especially in case of no transport-level security), or to validate the client in a single sign-on scenario, RS256 is a right choice. Here’s how to configure Flask-JWT for that:

  1. Generate an RSA key pair with openssl
openssl genrsa -out rs256.pem 2048
openssl rsa -in rs256.pem -pubout -outform PEM -out rs256.pub
  1. Install cryptography package which is not installed with Flask-JWT. Otherwise you’ll get

NotImplementedError: Algorithm not supported

  1. Configure RS256 in Flask settings
app.config['JWT_ALGORITHM'] = 'RS256'
app.config['JWT_SECRET_KEY'] = open('rs256.pem').read()
app.config['JWT_PUBLIC_KEY'] = open('rs256.pub').read()
  1. That should be it, however, Flask-JWT 0.3.2 has an implementation issue which would give

AttributeError: '_RSAPrivateKey' object has no attribute 'verifier'

with RS256 enabled. The reason is, it tries to use a private key for decryption instead of a public one. To fix that, you’ll need to supply your own jwt_decode_handler at JWT initialization:

from flask import current_app
import jwt as jwt_lib

jwt = JWT()

# JWT configuration code

@jwt.jwt_decode_handler
def rs256_jwt_decode_handler(token):
    secret = current_app.config['JWT_PUBLIC_KEY']
    algorithm = current_app.config['JWT_ALGORITHM']
    leeway = current_app.config['JWT_LEEWAY']

    verify_claims = current_app.config['JWT_VERIFY_CLAIMS']
    required_claims = current_app.config['JWT_REQUIRED_CLAIMS']

    options = {
        'verify_' + claim: True
        for claim in verify_claims
    }

    options.update({
        'require_' + claim: True
        for claim in required_claims
    })

    return jwt_lib.decode(token, secret, options=options, algorithms=[algorithm], leeway=leeway)

With that, you’ll have JWT authorization working in a normal way, but now with RS256 JWTs: