Joe Mosby's blog

Up and Running with Flask on a Brand New Linode

Tonight, I'm building a minimalistic Flask app that will run on Linode. Flask is a relatively new framework for me. I've dealt with CentOS, nginx, and uwsgi at work, but I've never tried to get them installed on my own. Time to get started.

Step 1: Install and configure nginx, round 1

I am skipping over the step where we create a CentOS 7 machine from the Linode dashboard. I can't think of a good way to explain that one without screenshots. Let's assume we have one, and let's install the necessaries on it.
$ yum install epel-release $ yum install nginx

Okay! That was simple enough. Here we've installed nginx, which will allow us to serve up our web pages. I want to start here because I like the feedback of knowing that I'm serving up web pages from the very start.

I know that I plan to serve multiple sites off of this one server, so I need to adjust my domain and nginx configuration accordingly. In Namecheap, I've created 2 A records - dashboard.mosby.io and www.mosby.io - which I'll both point at this server's IP address. I'll let nginx sort out the parsing.

# /etc/nginx/nginx.conf

... # stuff here that you shouldn't remove

http {

	... # more stuff here that you shouldn't remove

	server { 
		listen	80;
		server_name	www.mosby.io;
		root /var/www/main;
	}

	server {
		listen	80;
		server_name	dashboard.mosby.io;
		root /var/www/dashboard;
	}
}

nginx has a configuration file called nginx.conf that we're going to modify to serve our sites. Here, we've said that any request incoming will either be for www.mosby.io or dashboard.mosby.io. If it's for www, we're going to serve content from /var/www/main. If it's for dashboard, we're going to serve content from /var/www/dashboard. We can test this out by creating two text files:

# /var/www/main/index.html

Hello, www.mosby.io!

AND

# /var/www/dashboard/index.html

Hello, dashboard.mosby.io!

Assuming that you've already pointed your two subdomain to your Linode's IP address, these will each display their respective content. Success!

Step 2: Install Python3, Flask and uWSGI

This is the part I know least about this entire process. Let's start by installing Python 3, pip, and a version of virtualenv that's cool with all of this:
$ yum install python34 $ wget https://bootstrap.pypa.io/get-pip.py $ python3.4 get-pip.py

Now I'm going to move into my dashboard folder and create the application.

$ cd /var/www/dashboard
$ virtualenv venv
$ source venv/bin/activate

We've got a local instance of Python3 and pip now. Time to get uWSGI and Flask.

$ pip install uwsgi flask

Wait hold on. uWSGI just crapped out on installation. I missed something. The scary error message looks like this:

Command "/var/www/dashboard/venv/bin/python3.4 -c "import setuptools, tokenize;__file__='/tmp/pip-build-5a_chnf_/uwsgi/setup.py';exec(compile(getattr(tokenize, 'open', open)(__file__).read().replace('\r\n', '\n'), __file__, 'exec'))" install --record /tmp/pip-8zr4gl3_-record/install-record.txt --single-version-externally-managed --compile --install-headers /var/www/dashboard/venv/include/site/python3.4/uwsgi" failed with error code 1 in /tmp/pip-build-5a_chnf_/uwsgi

But that's not the root of the problem. I've got to scroll up the stack trace for that.

In file included from plugins/python/python_plugin.c:1:0:
plugins/python/uwsgi_python.h:2:20: fatal error: Python.h: No such file or directory
 #include <Python.h>

I don't have Python headers installed on this system! Let's see if I can do that.

$ yum install python34-devel
$ pip install uwsgi flask

Okay, that worked!

And now, let's convert our little test HTML from before into a Flask app. We're then going to remove the HTML file, which will let us confirm that we've actually set of Flask and uWSGI correctly when we see it again. I'm going to do some work here using the vi editor, but if you're not familiar with it, please replace the vi commands with nano. It's simpler.

$ vi app.py

from flask import Flask
application = Flask(__name__)

@application.route("/")
def helloworld():
	return "Hello, dashboard.mosby.io on Flask!"

if __name__ == "__main__":
	application.run(host='0.0.0.0')

$ rm index.html
$ python app.py

I can see that I've done it all right by going to my new homepage in my browser! Visiting dashboard.mosby.io:5000 will show me my updated page.

Step 3: Configure uWSGI Serving

We've got ourselves a basic skin of an application, so let's hook it up to nginx through uWSGI.

$ uwsgi --socket 0.0.0.0:5000 --protocol=http -w wsgi

It works! Exactly the same way it did when we ran the application directly. Let's build a .ini file so we can do this more repeatedly.

$ vi dashboard.ini

[uwsgi]
module = wsgi

master = true
processes = 5

uid = ghost
socket = dashboard.sock
chown-socket = ghost:nginx
chmod-socket = 660
vacuum = true

die-on-term = true

This ini file does a few things for us. It points to the wsgi module, sets it in master mode, and spawns 5 processes of the app. It also ties to the uWSGI process to a Unix socket, and will remove (vacuum) that socket when the process stops.

I'm also specifying that the ghost user will own this process. (I've been doing all of this work as root, which is not a good practice for running the application) I now need to create the ghost user and add it to the nginx group.

$ useradd ghost
$ usermod -a -G nginx ghost

Verify that you did it right:

$ id ghost

You should see the ghost user attached to the nginx group. Onward!

Step 4: Start on Boot

When our server comes online, we want our uWSGI app to be available immediately. Let's start by creating a service file in our /etc/systemd/system directory.

$ vi /etc/systemd/system/dashboard.service

[Unit]
Description=uwsgi instance to serve dashboard
After=network.target

[Service]
User=ghost
Group=nginx
WorkingDirectory=/var/www/dashboard
Environment="PATH=/var/www/dashboard/venv/bin"
ExecStart=/var/www/dashboard/venv/bin/uwsgi --ini dashboard.ini

[Install]
WantedBy=multi-user.target

There is black magic going on here that I need to dig into more. I don't know why we do all of these things, but I do know that we're specifying the working directory, and the environment, and what should happen when we start things up. Which we'll do now.

$ systemctl start dashboard
$ systemctl enable dashboard

Step 5: Proxy Requests from Nginx

Now it's time that we return to our nginx.conf file. We need to modify our dashboard server block to handle the uwsgi application.

# /etc/nginx/nginx.conf

... # stuff here that you shouldn't remove

http {

	... # more stuff here that you shouldn't remove

	server { 
		listen	80;
		server_name	www.mosby.io;
		root /var/www/main;
	}

	server {
		listen	80;
		server_name	dashboard.mosby.io;
		
		location / {
			include uwsgi_params;
			uwsgi_pass unix:/var/www/dashboard/dashboard.sock;
		}
	}
}

$ nginx -t <-- test your configuration
$ service nginx reload

And now we go to our browser and punch in dashboard.mosby.io, and... crap.

502 Bad Gateway. What did I do wrong here? That error means nginx can't talk to our application.

Ahh, I've been doing all this as root. Everything is currently owned by root, which means that neither ghost nor nginx can see my dashboard socket. Let's change that.

$ chown ghost:nginx /var/www/dashboard
$ service nginx reload
$ systemctl restart dashboard

And, we're back, folks!

That's a basic configuration for getting an Flask/uWSGI/nginx app up and running on a CentOS Linode box. If you decide to do something other than CentOS, most of it should still work, but the initialization service script will probably not. You can browse the repository for all of this here.