Deploy Python Flask to Production

Apache Foundation Feather Logo

If a tree falls in a forest and there are no clients, does it make a sound?

Deploy your web app production style. We'll use Apache2 mod_wsgi to install Flask "Hello world" app.

This is an intermediate article. If you want to learn Flask, you might want to start with Hello world.

Prerequisites: Hello Flask - Write a Python Web App and Linux command line and administration. Installing and configuring Apache web server.

We'll

  • Install Apache, the most popular web server in the world
  • Create a new Linux user for our app
  • Set up a new Apache Name Based Virtual Host with Python support
  • Place the app into our new user's home directory

When installing stacks with many components, method is important. It's practically impossible to always type a billion commands perfecdctly without any typos. So

  • Know your goal (e.g. get Apache response from localhost)
  • Always do the smallest separately testable part
  • Not tested == not done
  • Read the logs (/var/log/syslog, /var/log/apache2/error.log)
  • Make notes while you're working (otherwise you can only solve trivial problems)

Install Apache Web Server

Let's start with an Ubuntu Linux installed.

First, let's test that we're not running Apache web server yet. The port is closed (not open, not filtered), so we get a "connection refused" or similar answer immediately.

$ curl localhost
curl: (7) Failed to connect to localhost port 80: Connection refused

Or with Firefox http://localhost/ "Unable to connect".

Let's install Apache

$ sudo apt-get update
$ sudo apt-get -y install apache2

And test again

$ curl -s localhost
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 ...

Or with Firefox

Apache Foundation Feather Logo

We don't have to care about the content ("It works!"), but the fact that is a web page served by Apache shows that it's working.

Let's overwrite the test page with some placeholder content. You can use any neutral string.

$ echo "Learn Apache on TeroKarvinen.com"|sudo tee /var/www/html/index.html 

You can use 'sudoedit /var/www/html/index.html' if that is more familiar to you.

$ curl localhost
Learn Apache on TeroKarvinen.com

We can see that the test page was replaced. It looks the same with Firefox on http://localhost.

Create a New User for Our App

For security, we create a separate user for our app. The easiest user management commands start with a verb: adduser, deluser.

Always use a good password. Never use a bad password. Always practice using good passwords, because why on earth would someone practice doing things horribly wrong? If you can't come up with anything, you can randomize one

$ pwgen 30 1
leacho2ukooMea5eithei5thoh2egh

$ sudo adduser terowsgi

If you ever forget the password, you can easily overwrite the old one with

$ sudo passwd terowsgi

As we won't need to log in as this user, let's lock it. This prevents login with a password, and we have not enabled ssh public keys for this user.

$ sudo usermod --lock terowsgi 

Let's add ourselves to users own group. Adduser has another syntax to add user to a group. Yes, one command, quite different purposes.

$ sudo adduser $(whoami) terowsgi

Log out and back in to get activate this group.

Later, this will allow us to edit the files in this technical user's home directory.

Name Based Virtual Host for Python Flask

Create a new name based virtual host. We write the conf file in sites-available/, then enable it by creating a symlink in sites-enabled/.

$ sudoedit /etc/apache2/sites-available/terowsgi.conf
<VirtualHost *:80>
        ServerName tero.example.com

        WSGIDaemonProcess terowsgi user=terowsgi group=terowsgi threads=5
        WSGIScriptAlias / /home/terowsgi/public_wsgi/tero.wsgi

        <Directory /home/terowsgi/public_wsgi/>
                WSGIScriptReloading On
                WSGIProcessGroup terowsgi
                WSGIApplicationGroup %{GLOBAL}
                Require all granted
        </Directory>
</VirtualHost>

You might recognize the parts that are similar to any name based virtual host serving static content. We have VirtualHost, ServerName and Directory stanza.

New lines are highlighted. Most of this is boilerplate, same in many projects. The things that need changing are

  • User name, "terowsgi" here, repeated in many rows
  • Path to WSGI script

The meaning of each instruction is explained in Flask mod_wsgi deployment guide and mod_wsgi documentation.

Let's enable our new site to get some fresh errors. This simply modifies the symlinks in sites-enabled/.

$ sudo a2ensite terowsgi.conf
$ sudo a2dissite 000-default.conf

Settings are only taken to use after we kick the daemon. Before that, we can conveniently check that we did not make any typing misteaks.

$ apache2ctl configtest   # for Debian 10, use '/sbin/apache2ctl configtest'
AH00526: Syntax error on line 4 of /etc/apache2/sites-enabled/terowsgi.conf:
Invalid command 'WSGIDaemonProcess', perhaps misspelled or ..

Oh, maybe we should actually install the WSGI module before we can use it.

$ sudo apt-get -y install libapache2-mod-wsgi-py3
$ sudo systemctl restart apache2

$ apache2ctl configtest 	# for Debian 10, use '/sbin/apache2ctl configtest'
Syntax OK

Better.

Remember the py3 for Python3. Mistakenly installing the old Python 2 version creates very annoying error messages that don't often tell you you're using wrong Python version.

Apache2ctl configtest should say "Syntax OK". Any complaints about servername blah blah localhost are harmless. If there are actual errors, this usually tells the line number, too.

When restarting Apache, remember sudo. Often, systemd does something and tells you it did something if you forget sudo. It does not tell you that you forgot sudo.

Install Our App

So, as a reward for our efforts

$ curl localhost
... <title>403 Forbidden</title> ...
$ tail -1 /var/log/apache2/error.log 
... AH01630: client denied by server configuration: /home/terowsgi/public_wsgi

We just broke it? Maybe we should actually install our program. Note that the user (with curl or Firefox) just sees "403 Forbidden", but the actual filename is in the logs, just for us.

Let's create the folder it's complainging about. Verify that we're in the wsgi user's group

$ groups
... terowsgi

Let's create a the folder

$ sudo mkdir /home/terowsgi/public_wsgi

fixing the permissions, because normally we don't go around sudoing in user's home dirs

$ sudo chown terowsgi:terowsgi /home/terowsgi/public_wsgi

Let all users in terowsgi group edit the directory (g+rwx), and let's keep the terowsgi group for new files and folders (g+s).

$ sudo chmod g=rwxs /home/terowsgi/public_wsgi

So we have

$ ls -ld /home/terowsgi/public_wsgi
drwxrwsr-x 2 terowsgi terowsgi 4096 Feb 6 19:47 /home/terowsgi/public_wsgi

Let's collect our reward

$ curl -si localhost|grep title
<title>404 Not Found</title>
$ tail -1 /var/log/apache2/error.log 
... Target WSGI script not found or unable to stat: /home/terowsgi/public_wsgi/tero.wsgi

So now we fixed the directory problem, and we should create the tero.wsgi file.

$ nano /home/terowsgi/public_wsgi/tero.wsgi
import sys
assert sys.version_info.major >= 3, "Python version too old in tero.wsgi!"

sys.path.insert(0, '/home/terowsgi/public_wsgi/')
from hello import app as application

WSGI file is the entry point that starts our Python application. The most imporant things are adding the public_wsgi directory to Python path (sys.path.insert) and importing application.

I find it very annoying when my new, fresh Python 3 code is interpretted as Python 2, so I added an assertion that gives me a clean error if that happens.

Let's try again:

$ curl -s localhost|grep title
<title>500 Internal Server Error</title>
$ tail -1 /var/log/apache2/error.log 
... ModuleNotFoundError: No module named 'hello'

Wow, Apache would actually like to see my hello.py. Let's not keep her waiting. We can use our "Hello Flask" app, but we must remember to remove the insecure development server "app.run" line.

$ nano /home/terowsgi/public_wsgi/hello.py
from flask import Flask
app = Flask(__name__)

@app.route("/")
def hello():
	return "Learn Flask at TeroKarvinen.com!\n"

# Removed the app.run line
$ curl localhost
Learn Flask at TeroKarvinen.com!

Nice. Let's try with Firefox http://localhost. Note that in my screenshot there is a port number 8081, while your system likely shows just localhost.

Firefox shows Flask running with mod_wsgi

Do you see a message from Flask? Well done, you've just set your Flask up for production.

Living Fast & Dangerously

Load testing your own computer, in your own network, on your own hardware, is useful. Doing it to computers or networks you don't own could be denial of service attack - illegal. So be careful if you decide to try load testing tools. Here, localhost is a safe address.

That said,

$ ab -c 100 -n 10000 localhost/ # be careful
...
Time taken for tests:   4.611 seconds
Failed requests:        0
Requests per second:    2168.69 [#/sec] (mean)
Time per request:       46.111 [ms] (mean)
100%     74 (longest request)

So, with my single core machine with 1 GB RAM, Apache could serve over 2000 pages per second. The slowest page took less than 0.1 seconds to generate. Quite fast, but it's a tiny page.

When you start developing your app, you can compare results with templates and database access.

Happy web developing!

Adminstrivia

Apache Foundation Feather logo is their trademark.