Starting a Python Web App with Flask & Heroku
Pairing Flask with zero-effort container deployments is a deadly path to addiction.
It's difficult to cover every cloud solution on the market without at least mentioning Heroku. Heroku contrasts nearly every cloud hosting solution by offering a clear purpose: make deploying apps of any kind as easy as possible. Deploying to a VPS requires knowledge of web servers and configurations. Deploying to containers requires knowledge of Docker or Kubernetes. Deploying to Heroku requires nearly no prior knowledge of anything.
Heroku is great for getting MVPs out the door, or for devs who want to jump into developing web applications with knowledge of a specific language. Even developers with advanced knowledge of how to deploy production applications may want to use Heroku for fast internal deployments, or as a platform for "sketching out" a quick prototype.
In this exploration, we'll be using Heroku to deploy a Python application using the Flask framework.
We're on the topic of simplicity, so let's keep that theme going. Heroku's infrastructure offering is unique in that Heroku obfuscates the DevOps aspect of web development completely. That means that configuring web servers, managing Linux packages, and supplying SSL certs are entirely taken care of by Heroku.
Let's consider Heroku's ease-of-use services to be luxuries which save us time. They are NOT a replacement for grasping these concepts.
Aside from VPS upkeep, Heroku obfuscates the process of moving an app through development and production environments by defining pipelines. That's right, CI/CD is built directly into Heroku's interface.
The most addictive aspect of Heroku is probably the Elements marketplace. This is a place to window-shop for set-it-and-forget-it plugins for your app, most of which are very easy to integrate with.
Most add-ons fall under a few major categories: database resellers, analytics, and Redis, to name a few (interestingly enough, using the base Redis add-on in Heroku is free, while the equivalent instance would cost you 5 dollars from the same provider had you used them directly. Add-ons are "deployed" after a single click, and the ensuing configuration process varies from vendor-to-vendor after that.
Speaking of single-click, they handle single-click deployments of popular build packs, too. You, the thing that made DigitalOcean a big deal way back. You get the idea.
Creating your Project
Log in to the Heroku UI and create an app on a fresh Dyno. A Dyno is merely a fancy, overly branded word for "container." Next, you'll be prompted to download the Heroku CLI locally on your OS of choice, which is quick and painless. Now we're cooking with gas.
Create an empty local directory and type the following command to be prompted for your Heroku account credentials:
$ heroku login Enter your Heroku credentials. Email: firstname.lastname@example.org Password:
At this point, Heroku has already magically created a git repository for your application from which you'll be doing development from.
$ git clone https://github.com/heroku/example-flask-project.git $ cd example-flask-project $ heroku create Creating example-flask-project in organization heroku... done, stack is cedar-14 https://example-flask-project.herokuapp.com/ | https://git.heroku.com/example-flask-project.git Git remote heroku added
Wow, that sure looks a lot like we're working with Github huh? That's actually the point: if you so chose, you can configure the Heroku CLI to recognize your Github username with a simple
heroku config:get GITHUB_USERNAME=yourname. With this configured, Heroku will actually allow you to simply deploy to your personal Github repo and mimic the changes on your Dyno. Now let's configure this thing.
A Project For Ants
We're going to get started by building you obligatory "hello world" app. The resulting file structure is going to end up looking like this:
example-flask-project ├── app.py ├── Procfile ├── Pipfile ├── Pipfile.lock ├── runtime.txt ├── requirements.txt ├── Pipfile.lock └── setup.py
Note the existence of two files you may not have seen before if you're new to Heroku: the Procfile (no file extension) and requirements.txt. These are tiny files which specify which language we're using and how to start our app, but we'll get to that in a moment.
Managing Your Python Packages
Heroku impressively supports Pipenv out-of-the-box for handling and installing dependencies. Every time you deploy your application, Heroku will install the package version specified in Pipfile.lock to build your app from scratch. If you're new to using Pipenv consider quickly picking up the basics from this quick tutorial. If you're still using virtualenv, you should consider switching to Pipenv regardless.
Create a local folder for your project. In that folder, start a Pipenv shell:
$ pip install pipenv pipenv shell
With the shell activated, we can now install dependencies specific to our environment. At a bare minimum, we need to install two packages: Flask as our framework, and Gunicorn to run our app process.
(my-project)$ pip3 install flask gunicorn
Good job; now let's build out the files in our tree one-by-one.
The Procfile (no file extension) is a unique file to Heroku which is essentially a build command. This will be a one-liner to tell Gunicorn to startup our application from our base
web: gunicorn app:app
A quick breakdown here:
web is our process 'type'. other types exists, such as
clock, but that's not important for now.
app:app signifies looking for the 'app' module in our app.py file. If you'd like to move app.py to . a different folder down the line, this can be adjusted as such:
web: gunicorn differentfolder app:app
The runtime.txt file notifies Heroku of the language it's dealing with as well as the proper version. Heroku only supports up to a particular version of Python at any given moment (which is currently Python-3.7.1), but specifying a higher version will default to the latest version Heroku supports.
Even though Heroku uses your Pipfile to build dependencies, it's still best practice to keep a
requirements.txt present for numerous reasons. For example, if you remove dependencies from your Pipfile without uninstalling them,
requirements.txt is a useful way of identifying old packages in your environment that can be uninstalled.
(my-project)$ pip freeze > requirements.txt
As I'm sure you know,
pip freeze will print all packages and their versions into the designated file as such:
asn1crypto==0.24.0 bcrypt==3.1.4 beautifulsoup4==4.6.0 blinker==1.4 cffi==1.11.5 click==6.7 cryptography==2.2.2 Flask==1.0.2 Flask-Assets==0.12 Flask-Login==0.4.1 Flask-Mail==0.9.1 flask-mongoengine==0.9.5 Flask-SQLAlchemy==2.3.2 Flask-Static-Compress==1.0.2 Flask-User==22.214.171.124 Flask-WTF==0.14.2 gunicorn==19.9.0 idna==2.7 itsdangerous==0.24 jac==0.17.1 Jinja2==2.10 MarkupSafe==1.0 mongoengine==0.15.0 ordereddict==1.1 passlib==1.7.1 pycparser==2.18 pymongo==3.7.0 rjsmin==1.0.12 six==1.11.0 SQLAlchemy==1.2.9 webassets==0.12.1 Werkzeug==0.14.1 WTForms==2.2.1
Our Pipfile is automatically generated by Pipenv by default, but be sure to call out packages which are essential to the build our app as. Packages which are required for your app to work belong under the
[[source]] url = "https://pypi.org/simple" verify_ssl = true name = "pypi" [packages] gunicorn = "*" flask = "*" requests = "*" wtforms = "*" flask_assets = "*" flask_static_compress = "*" [dev-packages] [requires] python_version = "3.7.1"
Heroku looks at
Pipfile.lock every time our app builds to know which packages to install on the server side. Changing dependencies locally without updating the
Pipfile.lock will not carry the changes over to your Dyno. Thus, be sure to generate this file when needed:
(my-project)$ pipenv lock
Better yet, running the following will check your Pipfile for packages which can be updated, will update those packages, and then generate a lock file:
(my-project)$ pipenv update
Technically this file isn't required, but is a general best practice when creating projects. Most of
Setup.py's purpose comes in to play if you plan on submitting your project as a standalone package,
from setuptools import setup, find_packages setup( name='my-project', version='1.0', long_description=__doc__, packages=find_packages(), include_package_data=True, zip_safe=False, install_requires=['Flask'], )
Okay, okay, just one last thing. Heroku will be upset unless there's a
.env file in its root directory at run time.
.env is where we would store sensitive information (such as secrets), but feel free to leave this empty for now.
Heroku allows you to manage environment variables via their web UI as well. These can then be conveniently saved to your local environment to run your app locally, but let's stay focused on the task at hand: saying "hello" to the world.
Running your app locally is as simple as two words:
heroku local. This spins up an instance of your app on your machine at
Deploying to your Heroku Dyno is much like deploying to Github (they can in fact be the exact same if you configure it as such). Here's how deployment via the Heroku CLI looks:
git add . git commit -am 'initial commit' git push heroku master
If all went well, your app should be live at the URL Heroku generated for you when you created your project. Go ahead and checkout the Heroku UI to see how things went.
I highly suggest checking out the logs on the Heroku UI after each deploy. Often times issues which don't appear on your local environment will pop up on the server:
What Do We Make Of This?
There are two general takeaways I suppose I'm getting at:
- Heroku is easy and fun to use.
- Flask is awesome.
As much as #1 is true, I think it's important to distinguish Heroku's place in a crowded cloud market. Heroku is a platform best suited for dumping MVPs and side projects... NOT production applications. While you certainly can host large apps on Heroku, I consider it to highly unprofessional. Remember: Heroku is basically a reseller. They host their containers on AWS, and sell add-ons from other vendors. If you depend too heavily on Heroku, you are essentially just adding a middle man to your billing cycle.
On the Flask side: Flask's development may not be as vast as the
npm packages offered by Node, there's more or less a package for anything you possibly need. I'd recommend checking out Flask's official list of packages.
While we may have set up our first Flask application, as it stands we've only built something useless so far. Consider this to be the beginning of many, many Flask tips to come.