Skip to main content

Ubuntu 18.04.1, ISPConfig3, Python 3, Flask, Apache 2, and mod_wsgi

Today I spent a lot of time trying to figure out how to get a Flask application started using Python 3 on Ubuntu 18.04.1. I had previously built an application using Python 2.7, Flask, and mod_wsgi, but it had been a while and the documentation I came across just wasn't connecting the dots properly. Here's my notes after the endeavor.

For the example, the root directory of the ISPConfig3 user is going to be /var/www/clients/client1/web1/.

# install 
sudo apt install libapache2-mod-wsgi-py3 python3-pip
# disable mod_python if it's installed, or just uninstall it as mod_wsgi can't be loaded at the same time

# install python3 virtualenv globally
sudo pip3 install virtualenv

# change to the private folder where we'll create the project
cd /var/www/clients/client1/web1/private/
# create the project folder and change into it
mkdir project1 && cd project1
# create the virtual environment in the env directory, which will be created for us
virtualenv -p python3 env
# activate the virtual environment
source env/bin/activate
# install flask
pip install flask
# setup some other directories and empty files
mkdir app log
touch wsgi.py run.py config.py app/__init__.py

wsgi.py

import sys
sys.path.insert(0, "/var/www/clients/client1/web1/private/project1")

from app import app as application

run.py

# WSGI Server for Development
# Use this during development vs. apache. Can view via [url]:8001
# Run using virtualenv. 'env/bin/python run.py'
from app import app

app.run(host='0.0.0.0', port=8001, debug=True)

app/__init__.py

from flask import Flask
app = Flask(__name__)

# Configurations
#app.config.from_object('config')

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

Apache Directives for the WSGI configuration. This is to be added into the Options - Apache Directives section of the ISPConfig3 web interface. Note a change must be made to the ISPConfig3 vhost.conf.master file in order for the IfDefine statements to work. Diff included below. This is why I had to invert my logic and use IfDefine !ProtocolHTTPS instead of IfDefine ProtocolHTTP (see note below about the Define directive).

# For this to work /usr/local/ispconfig/server/conf/vhost.conf.master must be updated to include the (Un)Define ProtocolHTTPS commands
<IfDefine !ProtocolHTTPS>
WSGIDaemonProcess client1web1 python-home=/var/www/clients/client1/web1/private/project1/env python-path=/var/www/clients/client1/web1/private/project1
</IfDefine>
WSGIProcessGroup client1web1
WSGIScriptAlias / /var/www/clients/client1/web1/private/project1/wsgi.py
WSGIPassAuthorization On

<Directory /var/www/clients/client1/web1/private/project1>
    WSGIProcessGroup client1web1
    WSGIApplicationGroup %{GLOBAL}
	WSGIScriptReloading On
    Require all granted
</Directory>

This is an example the generated Apache virtual host configuration. This is not the complete ISPConfig3 generated config file as it has all sorts of other configuration options.

<VirtualHost *:80>
ServerName test.domain.com
ServerAdmin webmaster@test.domain.com

ErrorLog /var/log/ispconfig/httpd/test.domain.com/error.log

<IfDefine !ProtocolHTTPS>
WSGIDaemonProcess client1web1 python-home=/var/www/clients/client1/web1/private/project1/env python-path=/var/www/clients/client1/web1/private/project1
</IfDefine>
WSGIProcessGroup client1web1
WSGIScriptAlias / /var/www/clients/client1/web1/private/project1/wsgi.py
WSGIPassAuthorization On

<Directory /var/www/clients/client1/web1/private/project1>
    WSGIProcessGroup client1web1
    WSGIApplicationGroup %{GLOBAL}
	WSGIScriptReloading On
    Require all granted
</Directory>

</VirtualHost>

<VirtualHost *:443>
Define ProtocolHTTPS

ServerName test.domain.com
ServerAdmin webmaster@test.domain.com

ErrorLog /var/log/ispconfig/httpd/test.domain.com/error.log

<IfModule mod_ssl.c>
	SSLEngine on
	SSLCertificateFile /var/www/clients/client1/web1/ssl/test.domain.com-le.crt
	SSLCertificateKeyFile /var/www/clients/client1/web1/ssl/test.domain.com-le.key
	SSLCertificateChainFile /var/www/clients/client1/web1/ssl/test.domain.com-le.bundle
</IfModule>

<IfDefine !ProtocolHTTPS>
WSGIDaemonProcess client1web1 python-home=/var/www/clients/client1/web1/private/project1/env python-path=/var/www/clients/client1/web1/private/project1
</IfDefine>
WSGIProcessGroup client1web1
WSGIScriptAlias / /var/www/clients/client1/web1/private/project1/wsgi.py
WSGIPassAuthorization On

<Directory /var/www/clients/client1/web1/private/project1>
    WSGIProcessGroup client1web1
    WSGIApplicationGroup %{GLOBAL}
	WSGIScriptReloading On
    Require all granted
</Directory>

UnDefine ProtocolHTTPS
</VirtualHost>

Diff of the vhost.conf.master file.

# diff -u vhost.conf.master.1 vhost.conf.master
--- vhost.conf.master.1 2018-08-23 03:50:52.539521052 -0500
+++ vhost.conf.master   2018-08-23 03:43:36.470905313 -0500
@@ -12,6 +12,9 @@

 <tmpl_loop name='vhosts'>
 <VirtualHost {tmpl_var name='ip_address'}:{tmpl_var name='port'}>
+<tmpl_if name='ssl_enabled'>
+Define ProtoHTTPS
+</tmpl_if>
 <tmpl_hook name='apache2_vhost:vhost_header'>
 <tmpl_if name='php' op='==' value='suphp'>
                DocumentRoot <tmpl_var name='web_document_root'>
@@ -524,6 +527,9 @@

 <tmpl_var name='apache_directives'>
 <tmpl_hook name='apache2_vhost:vhost_footer'>
+<tmpl_if name='ssl_enabled'>
+UnDefine ProtoHTTPS
+</tmpl_if>
 </VirtualHost>

 <tmpl_if name='apache_version' op='>=' value='2.4' format='version'>
Still to do
  • Figure out how to configure this properly using just the ISPConfig3 control panel. I can make it work editing the config files by hand, but they eventually get overwritten by ISPConfig3. The big problem I have right now is the WSGIDaemonProcess directive can only exist once... not in both the http and https VirtualHost definitions. This was resolved as shown above.
Source material
  • coxley/flask-file-structure (Source)
  • proper structure of the wsgi.py file (Source)
  • Flask 1.0.2 Quickstart (Source)
  • Flask Deployment Options - mod_wsgi / Apache (Source)
Note regarding the Apache Define directive

"While this directive is supported in virtual host context, the changes it makes are visible to any later configuration directives, beyond any enclosing virtual host."

In order to workaround this behavior, I used a Define statement just after the opening <VirtualHost> tag, and an UnDefine statement just before the closing </VirtualHost> tag. This effectively maintains the scope of the Define to the VirtualHost.

(Source)