Deploy Django on Heroku the Easy Way

My momma always said, ‘Deploying is like a box of chocolates. You never know what the hell is going to happen.’
Deploying a Django application on Heroku should be easy and simple. And, to be honest, it is very simple. It is amazing how easy and cheap (actually, free) is to have your application deployed on production. However, it is complicated enough so the Internet is full of tutorials about how to do it.
Once you know how to do it, the information on Heroku development page suddenly seems obvious. But the first time… Oh my… Prepare yourself for wanting to throw your laptop through the window.
It’s not that it is a complex task. It is just that one line of code can make everything break, and Heroku build logs aren’t going to tell you what is going on. You ask them: hey, what’s wrong?, and they will say nothing. Really, what happens?, and the answer is you should know.
I am going to share the way of doing it that has worked the best for me. It is not the only way, but this one just works.
The most problematic issue: static files
What is so difficult about making Django and Heroku get along? Simple answer: static files.
Static files are all those non-HTML files (CSS, JavaScript, images and other files) that are part of your frontend. Without them, your web page would look boring as ****. How does the browser know that it should render the title of your page in pink? Because, in the header of your HTML file, there is probably a line that tells the browser what CSS file (the file that contains the visual style of your web page) to import:
<link rel=”stylesheet” type=”text/css” href=”/static/css/my_pink_style.css”>
or some other similar line.
When the browser receives a line like that, it will perform additional HTTP requests to the backend in order to download those files.
So, what’s the problem? The problem is that Django does not know how to serve static files.
Wait, what?
That’s right.
But I run Django locally and there is no problem with my web page style or animations.
That is right, but it just happens because in the settings.py
file, the DEBUG
flag is set to True
. In debug mode, Django implements a server for static files, but the moment you disbable it,
# settings.py
DEBUG = False
it will stop serving static files and, in addition to a very ugly page that renders the plain HTML, in the browser console you will get some errors similar to
GET http://127.0.0.1:8000/static/admin/js/nav_sidebar.js net::ERR_ABORTED 404 (Not Found)
In other words, Django does not know anything about the static
URL.
Usual Solution: Dedicated Web Server
The issue with serving static files in production is usually handled outside Django, by configuring the server manually. One would typically collect all static files in one folder by running the Django command
python manage.py collectstatic
This command will scan all folders where static files are placed (which folders it looks static files for can be configured in the STATICFILES_DIRS
variable in settings.py
) and copy them into the folder specified by the STATIC_ROOT
variable. The usual configuration is:
# settings.py
STATIC_ROOT = os.path.join(BASE_DIR, ‘staticfiles/’)
Then you must configure the web server (maybe the same server it’s running the Django application, but usually a different dedicated web server) to serve the collected files when a request contains your static URL ( STATIC_URL
variable in settings.py
), in the URL. For example, this can be done with an Nginx server or an Apache. In general, every production setup is different and will require slightly different steps.
More Compact Solution: WhiteNoise as Middleware
Heroku is a PaaS (platform as a service). This type of services allow developers to focus on their applications and not worry about infrastructure: server configuration, scalability… Therefore, we must look for another way to serve static files. The magic solution is called Whitenoise.
Whitenoise is a web middleware. According to the Django documentation:
Middleware is a framework of hooks into Django’s request/response processing. It’s a light, low-level “plugin” system for globally altering Django’s input or output.
In plain words, it is a piece of software that is executed before the request arrives to the view function in Django, and after the response is generated, so it can apply some preprocessing or postprocessing. This means that with a middleware we can check if the request URL is defining a static file (i.e. contains the STATIC_URL
) and in that case we don't forward the requiest to Django because it does not know what to do with that. Instead, we perform some other action to serve the requested file.
That is precisely what Whitenoise does (in addition to other cool functionalities like serving compressed content). They state it clearly on their documentation:
With a couple of lines of config WhiteNoise allows your web app to serve its own static files, making it a self-contained unit that can be deployed anywhere without relying on nginx, Amazon S3 or any other external service. (Especially useful on Heroku, OpenShift and other PaaS providers.)
This is exactly what we want. Installing and configuring WhiteNoise is not difficult and there are many tutorials that already explain how to do it. But in Heroku, things can be even simpler than that.
Fancy solution in Heroku: django-heroku
Deploying on Heroku requires more than just taking care of static files. You must also configure databases, logging, the allowed_hosts… There is a single library that takes care of all of that for you: django-heroku.
Once you install it, it will install automatically WhiteNoise and will configure all necessary variables automatically. Then, the only modifications you must include in your settings.py
are:
# settings.py
import django_heroku
DEBUG = False
django_heroku.settings(locals())
That’s it?
That’s it.
In case you want to use your own configuration for, let’s say, logging, you can disable the automatic configuration for that area:
django_heroku.settings(locals(), logging=False)
Deploying
Once you have your settings file modified in the previous way, you are almost there.
You also need to:
- Add django-heroku to
requirements.txt
- Add gunicorn to
requirements.txt
- Create
Procfile
- Deploy
You must install Gunicorn, the production web server that Heroku recommends for Django applications.
$ pip install gunicorn
Once installed, add it and django-heroku to requirements.txt
with:
$ pip freeze > requirements.txt
Then you also need a Procfile for declaring your applications process types and entry points. A simple yet perfectly working Procfile is
release: python manage.py migrate
web: gunicorn my_django_application.wsgi — log-file -
Finally, in order to deploy your code to Heroku, there are multiple options (we assume you have a Heroku account and have created a Heroku app). You could push your code to a GitHub repository and then you can use Heroku’s Connect to GitHub function. However, a faster way is to just commit your changes locally and push your changes to Heroku using Heroku Git.
To configure Heroku Git, you must first install Heroku CLI, and log in
$ heroku login
Then you can add a remote to your local repository with
$ heroku git:remote -a my_heroku_app_name
The last step is to just do the push
$ git push heroku master:main
and wait for the process to complete. If it has gone well, the output should look somewhat similar to
$ git push heroku master:main
Enumerating objects: 11, done.
Counting objects: 100% (11/11), done.
Delta compression using up to 8 threads
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 496 bytes | 496.00 KiB/s, done
Total 6 (delta 4), reused 0 (delta 0)
remote: Compressing source files… done.
remote: Building source:
remote:
remote: — — -> Building on the Heroku-20 stack
remote: — — -> Using buildpack: heroku/python
remote: — — -> Python app detected
remote: — — -> No Python version was specified. Using the same version as the last build: python-3.9.5
remote: To use a different version, see: https://devcenter.heroku.com/articles/python-runtimes
remote: — — -> No change in requirements detected, installing from cache
remote: — — -> Using cached install of python-3.9.5
remote: — — -> Installing pip 20.2.4, setuptools 47.1.1 and wheel 0.36.2
remote: — — -> Installing SQLite3
remote: — — -> Installing requirements with pip
remote: — — -> $ python manage.py collectstatic — noinput
remote: 129 static files copied to ‘/tmp/build_aafe4c62/staticfiles’, 411 post-processed.
remote:
remote: — — -> Discovering process types
remote: Procfile declares types -> release, web
remote:
remote: — — -> Compressing…
remote: Done: 60.8M
remote: — — -> Launching…
remote: ! Release command declared: this new release will not be available until the command succeeds.
remote: Released v13
remote: https://my-heroku-app-name.herokuapp.com/ deployed to Heroku remote:
remote: Verifying deploy… done.
remote: Running release command…
remote:
remote: Operations to perform:
remote: Apply all migrations: admin, auth, contenttypes, sessions remote: Running migrations:
remote: No migrations to apply.
remote: Waiting for release…. done. To https://git.heroku.com/my-heroku-app-name.git 4b7d12e..7fd2a31 master -> main
It is very important that you see that the collectstatic
command has been executed successfully
remote: — — -> $ python manage.py collectstatic — noinput
remote: 129 static files copied to ‘/tmp/build_aafe4c62/staticfiles’, 411 post-processed.
If you don’t see it, there is something that has gone wrong. Heroku build log is not very expressive regarding this issue, and if you don’t pay attention, you might think everything was fine. But then you go to the web page https://my-heroku-app-name.herokuapp.com/
(it is just an example I used, just use your app URL) and see some puzzling error like a 500 Internal Error and have no idea of what could have happened.
However, unless something tricky happens, the described process should be enough to deploy successfully your Django application to Heroku.
Originally published at https://www.listeningtothedata.com.