Django & Heroku: a match made in heaven?

Django & Heroku: a match made in heaven?

5 'gotchas' to be aware of when deploying a Django project on Heroku

My very first blog post on Hashnode was about the trials and tribulations of deploying a Django project on AWS. I knew then that my next project would use a different cloud hosting provider, and from what I'd read the choices for a small, low-budget hobby project basically boiled down to Heroku, which provides a platform-as-a-service (actually hosted on AWS), or DigitalOcean or Linode, which provide infrastructure-as-a-service. In the end, since I needed PostgreSQL and Redis for my website and Heroku provides both of these as managed add-ons, the choice was easy.

The process of deploying a Django app to Heroku is really quite simple and has been documented hundreds of times in different blog posts and tutorials (like this tutorial from Mozilla or the official Heroku documentation). Here I'm just going to give a rundown of 5 things that tripped me up during my deployment, in the hope that others might find them useful if they encounter the same problems.

1. Beware the misleading npm error

When I first tried to build my project on Heroku, the process to install the required node modules failed with an error along the lines of npm ERR! The 'npm ci' command can only install with an existing package-lock.json. This was weird, because I definitely had a package-lock.json, and it was definitely under version control. I tried deleting and regenerating it locally with npm install, but to no avail. Fortunately this StackOverflow answer came to the rescue: the problem was inconsistent nodejs versions in my local environment and on Heroku. The choice is to either specify a lower version number for Heroku (in your project's package.json) or upgrade your local nodejs and run npm install again. Either way it's an easy fix, but the npm error is extremely unhelpful!

2. Remember to run (and check) database migrations!

Because deploying to Heroku is so straightforward, it can be easy to overlook simple things. Heroku runs collectstatic for you when you deploy, but it does not make or apply migrations (and there will be no error at build time even if your migrations haven't been applied). When I first deployed my site, GET requests worked fine but every POST request returned 500 - Internal Server Error. It was only when I looked at my PostgreSQL database and saw that it was empty and contained no tables that I realised my mistake. I ran migrations and redeployed, but there was still a problem: running in debug mode I found out that one of the tables in my database was somehow missing a column, and I hadn't noticed any errors when I applied my migrations. A quick heroku pg:reset -a <app-name> and another round of makemigrations followed by migrate, and I was back in business.

3. The mystery of the missing Tailwind classes

This one was a bit of a head-scratcher: the vast majority of my CSS worked fine right out of the gate, but the styling on my forms was off. The Tailwind classes in my forms were definitely there, but they just weren't being applied. When I looked at the stylesheet in my browser and searched for the relevant classes, I saw that they were missing from the stylesheet generated at build time on Heroku, whereas they were present in my locally-built version. It finally dawned on me that the problem came from my use of the crispy-tailwind and django-crispy-forms packages: the just-in-time compiled stylesheet in my local development environment was able to resolve the classes used in my crispy forms, but the build process on Heroku, when it looked at the HTML, just saw {{ form|crispy }} or {% crispy form %} template tags and knew nothing about the Tailwind classes behind them. It turns out that this is a known issue and there is already a proposed workaround; I tried this, but wasn't able to get it to work, so resorted to the quick-and-dirty hack of building my stylesheet locally and committing it with the rest of the project.

4. You (don't) get what you (don't) pay for

My project uses django-celery-beat to run beat and worker processes to manage periodic tasks (updating renewal dates, sending reminder e-mails etc.). My Procfile originally looked as follows: web: gunicorn config.wsgi --log-file - --log-level info worker: celery -A config.celery_app worker -l info -P eventlet beat: celery -A config.celery_app beat -l info What I hadn't quite realised was that only one free-tier dyno per app is allowed on Heroku, so my web dyno was running quite happily but the worker and beat processes were just ignored. Again, no error or warning is raised at build time: you have to use heroku ps -a <app-name> to see which processes are actually running. Fortunately there is a tool you can use to fork processes within a single dyno: Honcho allows you to essentially use a proxy Procfile with the single process web: honcho start -f ProcfileHoncho to run multiple processes in a single dyno. The performance impacts are obviously a concern if you need fast response times around the clock, but for a small hobby project it works just fine. The other issue is that free-tier web dynos sleep after 30 minutes of inactivity, so your beat and worker processes will not run if there's not enough web traffic to keep your dyno awake. Since I needed to upgrade to a paid hobby-tier dyno anyway in order to apply my server-side SSL certificate from Cloudflare, this wasn't a problem for me.

5. Periodic tasks just don't work

I've no idea whether this last point is an issue with Heroku or rather with Celery: as mentioned, I have a beat and a worker process running, and I can define periodic tasks on the Django admin page, but such tasks (defined with a period of x seconds/minutes/hours starting from a given time) very rarely seemed to work. I had two tasks to run, each of which I wanted to run every 24 hours, but neither of them ever started. I was able to coax them to run at one point by setting the period to 3 minutes, but once I set it back to 24 hours they again went dark. Fortunately there is another type of task scheduler which meets my needs: I can use a crontab schedule to launch the task every day at a given time. Once I switched over from periodic to crontab scheduling, the tasks launched just fine!

So there you have it, five things which tripped me up when deploying Django on Heroku. Hopefully you can avoid the same mistakes! Overall I will say that the process was much less painful than deploying on AWS (especially as my AWS deployment was for a comparatively simple site). The site is now live at member-zone.net and the source code is available on GitHub; feel free to play around with the site and send me your feedback (but be gentle: I'm using a single hobby-tier dyno and a free database, so it won't support massive workloads or thousands of users!)

[EDIT 2023-01-04: due to increased hosting costs on Heroku, the site is no longer online; the source code is still available on GitHub.]