Flask

Search for '{{search_term}}'

Deploying Python Flask using uWSGI and Nginx on Ubuntu 14.04

CMOS is the Code-Maven Open Source podcast that also includes video interviews. Subscribe to this feed RSS feed with your Podcast listener app or via iTunes iTunes.

The following is a tutorial on how to set up uWSGI with an Nginx front end to serve a Flask based application.

In this tutorial we will only use the packages that are supplied by Ubuntu and we will deal with a very simple Hello World application.

It was tested on an Ubuntu 14.04 x64 droplet of Digital Ocean though a very similar process was used earlier on Ubuntu 13.10 for plain Python web application.

After you create a droplet with Ubuntu 14.04 x64 you'll get an e-mail with your IP address and the password of root. In this example I'll use 1.2.3.4 as the IP address. You'll have to replace the commands with the IP address of your server.

First just ssh to the server. On Linux/Unix/OSX you would type this:

$ ssh root@1.2.3.4

On Windows you'd probably install putty and use that.

You don't have to, but I recommend, the first thing you do after logging in to the server is to update the packages to the latest by typing the following:

# aptitude update
# aptitude -y safe-upgrade

Then reboot:

# reboot

This will disconnect you from the server. After a few seconds you can continue and connect again using ssh

Personally I have a public key on file at Digital Ocean that they install on the server when it is created so I did not have to do the following, but if you don't yet have a public key with them, I'd recommend copying your public ssh key to let you ssh without password:

$ scp ~/.ssh/id_rsa.pub root@1.2.3.4:.ssh/authorized_keys
$ ssh root@1.2.3.4

If the first command worked well, then the second won't ask for a password.

Hello World using Pyton Flask

Before the deployment options, let's write and try our Hello World using Flask.

Check the version of Python. I got the following:

# python --version
Python 2.7.6

Then create a user called dev so we won't do everything as root.

# adduser --gecos '' --disabled-password  dev

Switch to the new user and create a directory for the project.

# su - dev 
$ mkdir project
$ cd project/

In the project/ directory create a file called app.py with the following content:

from flask import Flask
import time 

app = Flask(__name__)

@app.route("/")
def main():
   return "Hello World at " + time.time().__str__() + "\n";

if __name__ == "__main__":
    app.run(debug=False)

This is slightly more complex than the Hello World in our stand-alone, as it also includes the timestamp to make it easy to observe that the code runs again on every reload of the browser.

Please also note, our app.run() call is protected by if __name__ == "__main__": so the code will only run immediately when this file is executed as a script and not when it is loaded as a module.

Let's run our application:

$ python app.py 
Traceback (most recent call last):
  File "app.py", line 1, in <module>
    from flask import Flask
ImportError: No module named flask

ImportError: No module named flask

Well, we can't use Flask without installing it, can we?

In order to make life a bit easier down the road I've opened a second terminal window and ssh-ed to the server in that window as well. Now I have one window where I am used "dev" and another window where I am user "root".

I've installed Flask provided and pre-packaged by Ubuntu. In the window where I am "root":

# aptitude -y install python-flask

Then I could switch back to my "dev" window and run the application again:

$ python app.py 
 * Running on http://127.0.0.1:5000/

in order to try it, I've switched to the "root" window and used curl to fetch the page:

curl http://127.0.0.1:5000/
Hello World at 1458888407.29

At this point I could ran the curl command a few more times to see how time is passing....

Anyway, I switched back to the "dev" console where I saw a new line on the screen:

127.0.0.1 - - [25/Mar/2016 04:47:43] "GET / HTTP/1.1" 200 -

This is just the server telling me about the request I generated using curl and that it served it well.

At this point I've also tried to access the server using my regular browser typing in http://1.2.3.4:5000/ but I have not received any response. Apparently the default server of Flask only listens on localhost (127.0.0.1).

I pressed Ctrl-C to stop the web server.

Install and setting up uWSGI

Now that we know our "application" works we can make the next step and serve it using uWSGI which is a much more robust server than the one used for our development.

First thing was to switch to the "root" window and install uWSGI and the Python Plugin for it:

# aptitude -y install uwsgi  uwsgi-plugin-python

Then, just for good measures, I've checked the version number:

# uwsgi --version
1.9.17.1-debian

Once I had uWSGI installed I've tried to launch it with the "Hello World application" on port 9090:

I've got a lot of output

$ uwsgi --http-socket :9090 --plugin python --wsgi-file app.py 

*** Starting uWSGI 1.9.17.1-debian (64bit) on [Fri Mar 25 02:56:14 2016] ***
compiled with version: 4.8.2 on 23 March 2014 17:15:32
os: Linux-3.13.0-79-generic #123-Ubuntu SMP Fri Feb 19 14:27:58 UTC 2016
nodename: flask
machine: x86_64
clock source: unix
pcre jit disabled
detected number of CPU cores: 1
current working directory: /home/dev/project
detected binary path: /usr/bin/uwsgi-core
*** WARNING: you are running uWSGI without its master process manager ***
your processes number limit is 3750
your memory page size is 4096 bytes
detected max file descriptor number: 1024
lock engine: pthread robust mutexes
thunder lock: disabled (you can enable it with --thunder-lock)
uwsgi socket 0 bound to TCP address :9090 fd 3
Python version: 2.7.6 (default, Jun 22 2015, 18:01:27)  [GCC 4.8.2]
*** Python threads support is disabled. You can enable it with --enable-threads ***
Python main interpreter initialized at 0x1723c80
your server socket listen backlog is limited to 100 connections
your mercy for graceful operations on workers is 60 seconds
mapped 72792 bytes (71 KB) for 1 cores
*** Operational MODE: single process ***
unable to find "application" callable in file app.py
unable to load app 0 (mountpoint='') (callable not found or import error)
*** no app loaded. going in full dynamic mode ***
*** uWSGI is running in multiple interpreter mode ***
spawned uWSGI worker 1 (and the only) (pid: 2128, cores: 1)

It seemed to be working sow I switched to the "root" window and using curl on port 9090 I've tried my luck:

root@flask:~# curl http://127.0.0.1:9090/
Internal Server Error

That does not look good.

In the meantime, in the "dev" window where I launched the server I've got some additional output:

--- no python application found, check your startup logs for errors ---
[pid: 4384|app: -1|req: -1/1] 127.0.0.1 () {24 vars in 247 bytes}
     [Fri Mar 25 04:59:12 2016] GET / => generated 21 bytes in 0 msecs (HTTP/1.1 500) 1 headers in 57 bytes (0 switches on core 0)

Stopped the server using Ctrl-C.

uWSGI needs an "application"

The key word both in this error message and in the initial output was application. Apparently uWSGI is looking for an object called "application" in our web, well, application.

There are a number of ways to solve this:

1) We can replace the word app by application in our code.

2) We can keep using @app, but also create a copy of "app" called "application" like this:

application = app  # make uwsgi happy

3) We can tell uWSGI that our "application" is called "app" by adding --callable app to our command line:

Using the new command line I've launched the server again:

$ uwsgi --http-socket :9090 --plugin python --wsgi-file app.py --callale app

This time, after switching to the "root" window, I got the expected response to the curl http://127.0.0.1:9090 command.

Now you can already visit the we site by following th URL: http://1.2.3.4:9090 (remember to replace the IP with the one you have).

Further uWSGI configuration (3 processes handle the requests) can be provided on the command line:

$ uwsgi --http-socket :9090 --plugin python --wsgi-file app.py --callable app --process 3

But, instead of the command line, it is probably better to create a configuration file called /home/dev/project/my-uwsgi.ini with the following content:

[uwsgi]
http-socket    = :9090
plugin    = python
wsgi-file = /home/dev/project/app.py
process   = 3
callable = app

Now we can launch the server using the following command:

uwsgi --ini project-uwsgi.ini

We can try it from the other window or from our own browser.

We can shut it down by pressing Ctrl-C.

We can then, using the "root" window, create a symbolic link so uWSGI will start our server automatically when the server boots up:

# ln -s /home/dev/project/my-uwsgi.ini /etc/uwsgi/apps-enabled/

and launch the service as root with the following command:

# service uwsgi start

At this time it might be worth to reload the page in our browser again, to make sure every thing still works.

Add Nginx to the mix

Install Nginx and remove the default configuration file:

# aptitude install nginx
# service nginx start
# rm /etc/nginx/sites-enabled/default

Instead of that create a new configuration file in /home/dev/project/my-nginx.conf with the following content:

server {
  location / {
    include uwsgi_params;
    uwsgi_pass 127.0.0.1:9090;
  }
}

Create a symbolic link in the directory of Nginx so when Nginx starts this configuration file is taken in account.

# ln -s /home/dev/project/my-nginx.conf /etc/nginx/sites-enabled/
# service nginx restart

Now you can visit http://1.2.3.4/ and see 502 Bad Gateway. What a bummer.

502 Bad Gateway

Looking at the error log of Nginx in /var/log/nginx/error.log I see the following:

upstream prematurely closed connection while reading response header from upstream, client: 192.117.127.193, server: ,
request: "GET / HTTP/1.1", upstream: "uwsgi://127.0.0.1:9090", host: "159.203.101.19"

Apparently what was missing was that I had to replace http-socket by socket in my-uwsgi.ini file. I did that and restarted uWSGI using

# service uwsgi restart

The application started to work!

Troubleshooting

There are two places to look for error messages: /var/log/nginx/error.log is the error log of Nginx and /var/log/uwsgi/app/my-uwsgi.log where the log of uWSGI can be found. Assuming you called the config file my-uswgi.ini.

For other options you might want to check out the Deploying Flask on uWSGI in the documentation.

Comments

In the comments, please wrap your code snippets within <pre> </pre> tags and use spaces for indentation.
comments powered by Disqus