Hosting multiple flask applications in nginx
Posted on 2017-09-24 in flask
This article describes configuration details related to running flask application from within nginx subdirectory using uwsgi.
This article turned out to be big and messy, and it deals with one specific configuration detail. For a simple/clean introduction to python/flask I'd recommend to check out this tutorial first: The Flask Mega-Tutorial
The original idea was to just outline the process, without going too far into configuration details.
Basically... some time ago I had to suddenly make and roll out a flask app, and at some point I decided that I want to write app in such way that it would run from any subdirectory with minimum configuration changes and definitely withotu any code changes. Unfortunately, I found out that there wasn't much information about that online, and some of information wasn't that good or was even overcomplicated.
After poking around, I've come up with somewhat sane solution.
My server was debian-based, meaning installing nginx and uwsgi was a matter of apt-get install
and all the configuration was sitting in /etc
folders.
To be specific, debian stores nginx configuration in /etc/nginx/sites-available
and /etc/nginx/sites-enabled
where sites-available
normally holds config files for sites, and sites-enabled
symlinks to the information from sites-available
.
Uwsgi is configured in the same manner, meaning config files are within /etc/uwsgi/apps-available
for available applications, and /etc/uwsgi/apps-enabled
stores symlinks to data from apps-available
for the active, or "enabled" applications.
Uwsgi configs are stored as ini files. Also they inherit values from /usr/share/uwsgi/conf/default.ini
.
On debian system both services can be restarted with service nginx restart
and service uwsgi restart
, which, depending on your configuration may or may not print some nicer messages in console.
In case you mess up configuration, the services will fail to restart and will get borked. In this case you'll go into /var/log/nginx
and /var/log/uwsgi
to read logfiles, and I believe some of it was available through systemctl.
Anyway.
To have a flask app running on nginx directory, you need to have a python script for the app, store it somewhere, then create uwsgi ini file, and entry within nginx configuration.
In my case uwsgi entry was similar to this:
[uwsgi]
plugin = http,python3
callable = app
virtualenv = /var/www/flask-virtualenv
mount = /app=/var/www/myapp/run.py
manage-script-name = true
pythonpath = /var/www/myapp
chmod-socket = 664
uid = www-data
gid = www-data
Now... some explanations.
plugin
line determines plugins necessary for unning apps. My applicaiton used python3, so I specified python3 module. http is necessary to make uwsgi talk with the app.callable
is a name of variable within flask app that represent applicaiton object. Those are present in any flask application, and may be named differently, but, for example, official flask tutorial names thsoe app, so you could do the same.virtualenv
is a virtual environment used to run the python script in question. You could read about virtual environments, for example, here, but in general those are tools used to create isolated python environment to run a script, and make sure that this environment is separate from the system-wide python installation. This is useful when you need to have different set of pip-installed modules available in your script, and those might be incompatble with your system-wide installation.Mount
point. Now this is the part which specifies in which directory on the server your application is going to run. In this case I specify that the application will be avaialble under/app
directory on the site.manage-script-name
- allows nginx to pass extra information into the app.pythonpath
- working directory for the script. If you're writing into files, sqlite3 datyabases, etc, This file determines where you app starts.chmod-socket
,uid
,gid
. Those define standard access rights for your app. See, services and python apps should not be run under almighty root account, so instead they're usually run as a restricted user. On debian systems this user iswww-data
. Also, communication with uwsgi app is done through a socket made within/run
directory, and if you want your app to work, this socket must be readablae forwww-data
user, or for whatever user is used by your configuration. This is what those settings are for.chmod
defines access rights for the socket, anduid
/gid
define group being used.
Now, nginx configuration. Interestingly, there was a lot of information online talking about using data passed through X-forwarded-proto and some of the examples were quite convoluted, but in practice those don't seem to be necessary.
I ended up with piece of configuration that only had:
location /app{
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/app/myapp/socket;
}
And that was it. Those lines needs to be added into nginx configuration, within server. I.e. it would look like this:
server {
....
location /app {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/app/myapp/socket;
}
....
}
However, there is one more piece of the puzzle. The manipulations outlined above will handle most of the requests,
including fetching images, accessing pages, etc. For example, if you try to access <mysite>/app/somefile
,
it will be routed into /somefile
, and passed into your flask app. Meaning for the most part your app won't need to know where it is located.
However, your app still needs to know where it is located on some occasions, for example in order to generate redirects.
For example, if there's <mysite>/app/login
page that redirects into
<mysite>/app/forbidden
page, or something like that, then app needs to know where it is located. Because if you simply generate redirect to /forbidden
, it'll go into directory above the one where you app is mounted,
and this directory may be handled by something else.
To deal with this, I utilized SCRIPT_NAME
variable which is set in your application when it is run by uwsgi. For this purpose I defined a getBaseUrl()
method which went like this:
def getBaseUrl():
scriptName = request.environ['SCRIPT_NAME']
if scriptName:
return scriptName
return ""
This returns a "base url" for application if it is set. Base url is determined by SCRIPT_NAME
parameter which is automatically set when your app is called by UWSGI.
In my case I simply plugged that url as a parameter for jinja templates rendering (render_template
), for example:
@app.route("/")
@app.route("/index")
def index():
return render_template('index.html', baseUrl=getBaseUrl())
and that handledredirects and alternative paths nicely. To read more about using jinja in flask, you could check out (official tutorial](http://flask.pocoo.org/docs/0.12/templating/).
And this is it. The article turned out to be longer and messier than I planned, but perhaps it will be useful to someone someday.