Missing Guide: Quart deployment

Reading Time: 6 min / Published: 21/3/2020 min /

Quart is a Python ASGI web microframework. It is intended to provide the easiest way to use asyncio functionality in a web context with python.

Quart is an up and coming python microframeork that is already production ready.

To deploy Quart you cannot rely on traditional python WSGI servers like apache's mod wsgi or uwsgi. Instead you need to rely on ASGI servers such as Hypercorn, Uvicorn or Gunicorn. All commands mentioned in this guide are run on debian buster (10) with the latest updates. The commands were tested on ubuntu too. Your commands may vary for e.g. user creation.

But those servers shouldn't be directly exposed to the user for their lack of e.g. load balancing … For these functions nginx or apache2 are good and reliable options.

In this guide we wil cover apache2 and nginx with reverse proxy configurations. The used ASGI server is Hypercorn due to it's support for features such as HTTP/2.

We will also create a systemd service for managing our application. For the sake of this guide we assume you operate on a systemd based system.

To start with a simple project we will use a simple quart app.

app.py:

from quart import Quart

app = Quart(__name__)

@app.route("/")
async def index():
    return "Hello World"

if __name__ == "__main__":
    app.run()

If you run this app you will receive a simple text response on "http://127.0.0.1/". But this example runs on the integrated development ASGI server of Quart. To run this application with hypercorn.

Just execute python3 -m hypercorn app:app and hypercorn will run your application. Easy, right? Now we can begin with setting up an environment or you application. Begin with creating a new user (Guide user: hypercorn-executor).

You may want to disable ssh access for this user.

sudo adduser hypercorn-executor

Now you can start moving the project files to a dedicated folder. For this guide we will use /var/www/your-project. We're now ready to create the systemd service unit.

/etc/systemd/system/<your-service-name>.service:

[Unit]
Description=<your project> hypercorn service
After=postgresql.service
StartLimitIntervalSec=0

[Service]
Type=simple
Restart=always
RestartSec=1
User=<your dedicated user>
WorkingDirectory=<your dedicated project directory>
ExecStart=python3 -m hypercorn app:app

[Install]
WantedBy=multi-user.target

You may adjust this service unit to e.g. be executed after an other service such as mysql.service. Make sure to set the file permissions on your dedicated project directory to your new user. You replace hypercorn at this point with another ASGI server when required.

sudo chown -R <your dedicated user>:<your dedicated user> <your dedicated project directory>

You should at this point log into your dedicated user:

su <your-dedicated-user>

And install your dependencies and hypercorn:

python3 -m pip install hypercorn quart

Now make sure your app is still executable:

python3 -m hypercorn app:app

Now logout and reload the systemd daemon:

systemctl daemon-reload

And start your service:

sudo service <your-service-name> start

And your service is up and running. When something goes wrong e.g. a missing dependency see systemctl status <your-service-name>.service. Now you can tray adjusting the hypercorn options (worker, loop …) to your system.

Check if your service can start if not move it away from /etc/systemd. It can brick your system

You can finally setup the webserver of your choice (nginx or apache2) with a reverse proxy config.

nginx example based on Gunicorn Documentation:

worker_processes 1;

user nobody nogroup;
# 'user nobody nobody;' for systems with 'nobody' as a group instead
error_log  /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
  worker_connections 1024; # increase if you have lots of clients
  accept_mutex off; # set to 'on' if nginx worker_processes > 1
  # 'use epoll;' to enable for Linux 2.6+
  # 'use kqueue;' to enable for FreeBSD, OSX
}

http {
  include mime.types;
  # fallback in case we can't determine a type
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log combined;
  sendfile on;

  upstream app_server {
    # fail_timeout=0 means we always retry an upstream even if it failed
    # to return a good HTTP response

    # for UNIX domain socket setups
    server unix:/tmp/gunicorn.sock fail_timeout=0;

    # for a TCP configuration
    # server 192.168.0.7:8000 fail_timeout=0;
  }

  server {
    # if no Host match, close the connection to prevent host spoofing
    listen 80 default_server;
    return 444;
  }

  server {
    # use 'listen 80 deferred;' for Linux
    # use 'listen 80 accept_filter=httpready;' for FreeBSD
    listen 80;
    client_max_body_size 4G;

    # set the correct host(s) for your site
    server_name example.com www.example.com;

    keepalive_timeout 5;

    # path for static files
    root <your dedicated project folder static folder>;

    location / {
      # checks for static file, if not found proxy to app
      try_files $uri @proxy_to_app;
    }

    location @proxy_to_app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
      proxy_set_header Host $http_host;
      # we don't want nginx trying to do something clever with
      # redirects, we set the Host: header above already.
      proxy_redirect off;
      proxy_pass http://127.0.0.1:8000;
      # Port may vary with hypercorn configuration
    }

    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root  <your dedicated project folder public folder>;
    }
  }
}

You may want to disable access logging for privacy and performance reasons

Apache2 Reverse Proxy:

<VirtualHost *:443>
    Servername example.org

    # Reverse Proxy
    ProxyRequests Off
    <Proxy *>
            AddDefaultCharset Off
            Order deny,allow
            Allow from all
    </Proxy>
    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:8000/
    ProxyPassReverse / http://127.0.0.1:8000/
    # May vary depending on hypercorn options

    # Add 500 fallback
    ErrorDocument 500 <your dedicated project folder public folder>/500.html
</VirtualHost>

You're now ready to rock. For more information take a look at my references.

My References: