Wsgi Through Flask: A DreamHost Adventure

tythos

Brian Kirkpatrick

Posted on March 2, 2022

Wsgi Through Flask: A DreamHost Adventure

I regularly rely on my steady & trustworthy VPS, run by some old friends over at DreamHost.

https://www.dreamhost.com/

But, my development habits have changed a lot over the years. Even just within my Python years, I've gone from CherryPy to Tornado to... well, suffice it to say, I'm currently using Flask a lot, and it's great. You should use it, too.

But, DreamHost is largely an Apache-oriented hosting service that defaults to PHP. Ruby and Python are supported, too, but only through Passenger loading. So, how can we take a minimal web framework tool like Flask and use it to host services here? Easy!

Set Up A New Subdomain

Infinite subdomains are an amazing feature that I can't get enough of. (I should probably have fewer subdomains. But that's another fight for another day.) The first step is to spin one up to give yourself an experimental sandbox:

  • Make sure Passenger is enabled

  • Create a new user for your subdomain

  • Once created, make sure the user has SSH enabled

As always, be patient while registration and systems settings propagate! Shouldn't be more than a few minutes. Then, you should be able to SSH into the new subdomain and get hacking.

Bootstrapping From Passenger

The first thing you need to do is organize the Python environment. Once upon a time, using Python 3 (you ARE using Python 3, aren't you?) required a manual fetch-and-build process. Recent upgrades have ensured it is available on all systems--but not by default! OS constraints mean you have to use "python3" and "pip3" commands.

But that's not a huge problem. It just means we have to ensure Apache's Passenger logic is routed through the modern interpreter. You can resolve the path easily enough:

which python3

(It will mostly likely give you "/usr/local/python3", but you can't be too sure.)

Passenger will route through the "passenger_wsgi.py" script in your domain-specific folder, so change to that directory now and create that file. It will have three responsibilities, after importing the "os" and "sys" modules:

  • Defining the desired interpreter path (replace with your value if it differs)

INTERP = "/usr/local/python3"

  • Forwarding to that interpreter if it isn't being used already (os.execl is a neat trick, if you haven't seen it before)
if sys.executable != INTERP:
    os.execl(INTERP, INTERP, *sys.argv)
Enter fullscreen mode Exit fullscreen mode
  • Load the "application" symbol from the "application_server.py" module, which we'll define later

from application_server import APP as application

The "application" symbol is a magic WSGI application symbol that Passenger will look for once this module is imported. Don't ask questions! Just believe!

Define Your Flask App

In a new file, "application_server.py", we define our Flask application--which just happens to be a fully-compatible WSGI entry point. (Didn't I tell you Flask was neat?) You'll need three imports:

import os
import flask
from gevent import pywsgi
Enter fullscreen mode Exit fullscreen mode

Next, define your module-level variables, including the Flask application (remember, we imported "APP" from the "passenger_wsgi.py" script):

APP = flask.Flask("wtf")
SERVER_HOST = os.getenv("SERVER_HOST", "0.0.0.0")
SERVER_PORT = int(os.getenv("SERVER_PORT", "8000"))
Enter fullscreen mode Exit fullscreen mode

What are SERVER_HOST and SERVER_PORT doing? Good question. We'll get to that, I promise.

Next, you just need a basic Flask route:

@APP.route("/")
def index():
    """
    Root endpoint
    """
    lines = [
        b"This is a test!",
        b"If you can read this, Flask is successfully serving a WSGI application through Passenger.",
        b"This means you are awesome; congratulations!"
    ]
    return b"\n".join(lines), 200, {"Content-Type": "text/plain"}
Enter fullscreen mode Exit fullscreen mode

We'll throw in a local-hosting entry point here, using the aforementioned SERVER_HOST and SERVER_PORT to support environmental variables (say, from a container configuration), in the (very likely) case we'll want to run this server locally for development and testing purposes:

def main():
    """
    By default, hosts locally w/ gevent on port 8000
    """
    pywsgi.WSGIServer((SERVER_HOST, SERVER_PORT), APP).serve_forever()
Enter fullscreen mode Exit fullscreen mode

And lastly, when invoked from the command line, we'll want to call the "main()" function (because a self-contained scope is a happy scope!):

if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

This is the real magic, and (obviously) the place where you can run wild with whatever your Flask application needs to do. Everything else is just housekeeping and route/configuration management.

Dependencies

We've used two specific dependencies, so prudence demands we document them in a "requirements.txt" file; I find it useful to constrain by minor version (juicy semver logic here), using my local dev/test environment's packages to control versions:

flask >= 1.1
gevent >= 20.9
Enter fullscreen mode Exit fullscreen mode

One tricky part here is, the standard VPS doesn't have the permissions (or shared system space) to just pip-install (or pip3-install!) straight from a requirements file. Instead, you need to do a user-level installation using a user-level temporary directory.

mkdir ~/tmp
TMPDIR=/home/my_domain_user/tmp pip3 install --user -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

But once that's done, there's only one more step to go.

Touch Me Already

Apache watches a tmp/restart.txt file under your subdomain folder, which doesn't exist yet, in order to trigger Passenger reloads. So, create that folder and touch the file:

mkdir tmp
touch tmp/restart.txt
Enter fullscreen mode Exit fullscreen mode

And that's it! You should be able to browse to your subdomain and enjoy your self-defined reward message.

Where Do We Go From Here

If you want, or need something to reference, you can clone the following GitHub project right into your subdomain folder to compare against my "reference" implementation:

https://github.com/Tythos/wtf

Flask is a lightweight, but insanely powerful, server tool. You can define static applications, dynamic routes, templated (or full-up MVC) logic, and even support streaming WebSocket endpoints. Once you have this configuration up and running, all of these things become possible with your basic, out-of-the-box VPS, and the world becomes your oyster. Enjoy!

💖 💪 🙅 🚩
tythos
Brian Kirkpatrick

Posted on March 2, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related