Running Flask app with gunicorn using a Flask-Script command (on Heroku)

About two weeks ago I felt a huge need of learning something new – a language, framework… Anything! Just to make my brain work a bit harder. After looking around for a few days (sorry Scala and Erlang, you have to wait a bit longer!) I decided to become more familiar with modern cloud application platforms which are becoming more popular these days (or Paas model in general). Because I think that real projects are much better that rewriting tutorials and reading docs, I decided to write a small Flask web app and deploy it on Heroku. After a few days of learning Flask, coding and running my app on a built-in Flask server I decided to move to a bit more production-ready stage by running it using Gunicorn. And here the story begins…

So, how to make WSGI app work together with gunicorn? In the basic case it’s simple – just run:

gunicorn appmodule:app

That’s it – it will work. However, I developed my app using application factory “pattern” (function that returns Flask app instance; Flask-Script Manager uses it), which looks like this:

1
2
3
4
def create_app(environment='development'):
    app = Flask(__name__)
    ...
    return app

When used together with Flask-Script, it provides a nice way of managing your working environment. For example you can easily your development server by using commands like this one:

python manage.py runserver --environment development

where environment is a parameter that lets you easily define the configuration you are going to use. Or, as I’m talking about Heroku here too, if you want to have your development environment to be similar to the Heroku’s one and you use foreman – you may place this comand in Procfile:

web: python manage.py runserver --environment development

It’s a very nice solution, but – unluckily – it doesn’t have an out-of-the-box support for anything else than the default Flask development server, so I couldn’t easily define new command like rungunicorn and start my app using:

python manage.py rungunicorn --environment production --gunicorn-config gunicorn.ini

The only thing I could do was:

gunicorn mymodule:create_app\(environment=\"production\"\)

which is very ugly and has nothing to do with manage.py script – unacceptable for me. This is why I decided to play a bit with Flask-Script and Gunicorn, basing on Flask-Action extension code, to write my own command that will handle this in the way I want it to work. Here is the result of my work:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Gunicorn(Command):
    description = 'Runs production server with gunicorn'
 
    def __init__(self, host='127.0.0.1', port=5000, **options):
        self.port = port
        self.host = host
        self.server_options = options
 
    def get_options(self):
        options = (
            Option('--gunicorn-host',
                   dest='host',
                   default=self.host),
 
            Option('--gunicorn-port',
                   dest='port',
                   type=int,
                   default=self.port),
 
            Option('--gunicorn-config',
                   dest='config',
                   type=str,
                   required=True))
 
        return options
 
    def handle(self, app, host, port, config, **kwargs):
        from ConfigParser import ConfigParser
        gc = ConfigParser()
        gc.read(config)
        section = 'default'
 
        bind = "%s:%s" % (host, str(port))
        workers = gc.get(section, 'workers')
        pidfile = gc.get(section, 'pidfile')
        loglevel = gc.get(section, 'loglevel')
 
        # Suppress argparse warnings caused by running gunicorn's argparser
        # "inside" Flask-Script which imports it too...
        warnings.filterwarnings("ignore", "^.*argparse.*$")
 
        from gunicorn import version_info
        if version_info >= (0, 9, 0):
            from gunicorn.app.base import Application
 
            class FlaskApplication(Application):
                def init(self, parser, opts, args):
                    return {
                        'bind': bind,
                        'workers': workers,
                        'pidfile': pidfile,
                        'loglevel': loglevel
                    }
 
                def load(self):
                    return app
 
            # Hacky! Do not pass any cmdline options to gunicorn!
            sys.argv = sys.argv[:2]
 
            print "Logging to stderr with loglevel '%s'" % loglevel
            print "Starting gunicorn..."
            FlaskApplication().run()
        else:
            raise RuntimeError("Unsupported gunicorn version! Required > 0.9.0")

And the gunicorn.ini:

[default]
workers = 1
pidfile = tmp/districtrank.pid
loglevel = debug

Two things:

  1. Because gunicorn uses argparse too, I had to remove the Manager’s sys.argv params – gunicorn will crash as it could not recognize them. It could have been done in a nicer way, but for my needs it’s perfectly enough
  2. I’m suppressing argparse warning caused by some import issues to have clean output. OK, supressing warnings is bad, but I think in this case it’s justified.

To use it, place it somewhere in your application (remember to add imports), import the class in manage.py and register a new Manager’s command:

manager.add_command("rungunicorn", Gunicorn(host=host, port=port))

Now you can use the newly created command, passing a path to config file to it.

It’s definitely NOT a piece of code that could get into the Flask-Script codebase (it fits my needs, but is not very generic and may require changes; moreover it uses an ugly sys.argv hack and supresses argparse import warnings), but it works exactly in the way I want it to work, so I can start my app on gunicorn with:

python manage.py rungunicorn --environment production --gunicorn-config gunicorn.ini

Works for me! ;-)

Comments are closed.