Brian Kirkpatrick
Posted on March 2, 2022
I regularly rely on my steady & trustworthy VPS, run by some old friends over at DreamHost.
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)
- 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
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"))
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"}
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()
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()
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
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
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
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:
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!
Posted on March 2, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.