Get listed

Django on Heroku: DEBUG off, real SECRET_KEY, exact hosts

A packed teal suitcase with a hanging coral luggage tag.

A Django deploy is a list of settings that must be true in production, not a git push that happened to work.

Debug left on, a secret in the repo, and cookies that can travel on HTTP are the usual PaaS misses. Heroku’s own future is a separate question from whether your settings.py is safe on whatever host you use next.

The usual mistake is treating platform defaults as a security review. The platform gives you TLS at the edge. It does not set SECURE_SSL_REDIRECT or rotate your secret.

This page is the Django production checklist that still applies on Heroku or anywhere else, and the secrets that must not ride the slug.

February 6, 2026 is the date Nitin T Bhat posted An Update on Heroku. Salesforce moved the PaaS to sustaining engineering: stability and security patches, no new feature train, no new Enterprise Account contracts. August 4, 2026 is the date Django 5.2.17 shipped. startproject still writes DEBUG = True and a django-insecure- key. Those two facts still collide on a dyno.

The control is a production settings module the process cannot boot without, a host list you can spell, TLS flags the router header matches, and a check command that prints zero security warnings.

Pair this page with Django XSS for |safe and mark_safe after the deploy. Flask has the same missing-key shape: see Flask locks. Keep the secure coding checklist next to both.

Heroku is in sustaining mode. The settings did not shrink

first-party post. Two sentences do the work this page needs. Heroku remains supported and production-ready, with emphasis on quality rather than new features. Enterprise Account contracts will no longer be offered to new customers. Existing Enterprise subscriptions may renew. Dashboard credit-card customers, existing and new, keep pricing and day-to-day use.

Do not wait for one before you write the settings. Do not sell a new product as “Heroku-native” without a written exit. The four Django knobs below are the same on Fly, Render, Railway, or a VM. The host only changes how you inject them.

Heroku config vars are process environment. The Configuring Django Apps for Heroku page, last updated 22 March 2025, still says to read those vars in settings.py and to declare a Procfile. It does not ship a secure settings module for you.

FactAs of
Sustaining engineering, no new Enterprise6 Feb 2026, heroku.com/blog
Django 5.2.17 LTS patch4 Aug 2026, djangoproject.com
5.2 extended supportApril 2028 on the download page
Default Python on new apps3.14, Dev Center 20 Feb 2026
Python 3.8 on HerokuNo longer supported

DEBUG=False or the traceback is the product

5.2 deployment checklist. “You must never enable debug in production.” The page names what True leaks: excerpts of source, local variables, settings, libraries. That includes DJANGO_SECRET_KEY if it is in os.environ when the exception page renders.

startproject writes DEBUG = True. A dyno that imports that module as-is is a public traceback. Do not read a missing env as true. Do not leave a fallback of 1.

# invoiceapp/settings/prod.py
import os

def env_flag(name):
 raw = os.environ.get(name, "")
 if raw in ("0", "false", "False", ""):
 return False
 if raw in ("1", "true", "True"):
 return True
 raise RuntimeError("set %s to 0 or 1" % name)

DEBUG = env_flag("DJANGO_DEBUG")
if DEBUG:
 raise RuntimeError("DJANGO_DEBUG must be 0 on this module")

Local laptops import invoiceapp.settings.dev. The dyno sets DJANGO_SETTINGS_MODULE=invoiceapp.settings.prod. One module. No if not DEBUG sandwich that a second import path can skip.

Run the deploy check against that module before the git push:

DJANGO_SETTINGS_MODULE=invoiceapp.settings.prod \
DJANGO_DEBUG=0 \
DJANGO_SECRET_KEY=unused-for-check \
ALLOWED_HOSTS=invoice-web.herokuapp.com \
python manage.py check --deploy

Zero security warnings is the gate. A leftover security.W018 about DEBUG means the dyno will still serve the yellow page.

SECRET_KEY from config, crash if missing

The same checklist says the key must be a large random value, kept secret, unused elsewhere, and not committed. The documented load is os.environ["SECRET_KEY"], a KeyError if unset. os.environ.get("SECRET_KEY") returns None. Django 5.2 will then refuse to start in some paths and silently accept a weak value in others. Fail closed.

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
if SECRET_KEY.startswith("django-insecure-"):
 raise RuntimeError("replace the startproject key")
if len(SECRET_KEY) < 50:
 raise RuntimeError("DJANGO_SECRET_KEY is too short")

Mint it on a laptop you own. Put it only in config vars. Never in git, never in a screenshot of the dashboard, never in the Procfile.

python -c "import secrets; print(secrets.token_urlsafe(64))"
heroku config:set DJANGO_SECRET_KEY='paste-the-token' -a invoice-web
heroku config:set DJANGO_DEBUG=0 -a invoice-web

If that value ever sat in a public repo, rotate. Django 5.2 documents SECRET_KEY_FALLBACKS for a staged rotate: current key first, old key in the list, then drop the old key after sessions re-sign. that paragraph on the checklist. Remove the fallback on a timetable, not “later.”

Database URLs and third-party tokens follow the same rule. DATABASE_URL is already a config var when you attach Heroku Postgres. Parse it. Do not paste a postgres URI into prod.py.

ALLOWED_HOSTS is a list of names you own

The checklist: when DEBUG is false, Django does not work without a suitable ALLOWED_HOSTS. The setting exists to stop Host-header tricks. A wildcard means you must validate Host yourself.

That leading-dot form matches every hostname under the registrable domain. It is not “my app.” It is “any name that ends that way.” Name the hosts on the certificate.

ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
# invoice-web.herokuapp.com,www.example.com
# never *, never.herokuapp.com
heroku config:set \
 ALLOWED_HOSTS=invoice-web.herokuapp.com,www.example.com \
 -a invoice-web

Add the custom domain only after heroku domains shows it. Do not add a preview-app wildcard because a review app was convenient. Each review app gets its own name and its own config.

Four gates on invoice-web. A miss at any line is a 400 or a refused boot.
BROWSER
 |
 v
 ACM TLS heroku certs:auto
 |
 v
 Router Host must be a name you listed
 |
 v
 gunicorn invoiceapp.wsgi
 |
 +-- DEBUG is 0 or the process dies
 +-- DJANGO_SECRET_KEY from config
 +-- ALLOWED_HOSTS exact names
 +-- SECURE_* flags below
 |
 v
 200 on https://www.example.com

TLS: ACM on the edge, Django flags on the cookie

Automated Certificate Management, last updated 7 July 2026. ACM is free on Common Runtime apps. heroku certs:auto:enable is the command. Status Cert Issued is the done line. Custom-domain DNS must point at the herokudns.com target from heroku domains, not at *.herokuapp.com.

*.herokuapp.com already presents TLS. HTTP on the same hostname still answers unless something redirects. Django must do that redirect, and it must trust the proxy header Heroku sets.

5.2 SECURE_PROXY_SSL_HEADER docs. Behind a proxy, set:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_CONTENT_TYPE_NOSNIFF = True

The docs warn that SECURE_SSL_REDIRECT = True without the proxy header loops, because Django sees HTTP from the router. Set the header first. Then the redirect. Then HSTS. Do not start HSTS at a year on a hostname you still open over HTTP from a second CDN. Start at a day on a staging app you own, then raise it.

The checklist also wants SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE so the session never rides HTTP. That is the cookie half. Template XSS after the cookie is set is a different page: Django XSS.

Python 3.14 and a real Procfile

Python Support Reference, last updated 20 February 2026. New Python apps default to the latest 3.14 patch. Supported majors: 3.14, 3.13, 3.12, 3.11. 3.10 is deprecated. 3.9 and 3.8 are no longer supported. A runtime.txt that still names python-3.8.11 will not build. runtime.txt itself is deprecated. Write .python-version with a major, so patches apply on the next build.

#.python-version
3.14
# Procfile
web: gunicorn invoiceapp.wsgi --log-file -

The March 2025 Django config article still names gunicorn as the process. Pin gunicorn in requirements.txt next to Django>=5.2.17,<5.3. Do not run manage.py runserver on a dyno. The checklist says switch away from runserver for production.

Do not set DISABLE_COLLECTSTATIC=1 to “make the push work.” That was the 2021 shortcut. Define STATIC_ROOT, run collectstatic in the build, and serve with WhiteNoise 6.x or a CDN you control. A 500 on /static/admin/ is how people flip debug back on.

There is no current first-party replacement for the archived django-heroku package. Read config vars yourself, as the Dev Center page already tells you to. A helper that mutates settings at import time is how DEBUG sneaks back.

Prove the dyno before you share the URL

You are reading your own app. You are not scanning other people.

  1. Set DJANGO_SETTINGS_MODULE=invoiceapp.settings.prod on invoice-web.
  2. Run heroku config -a invoice-web. You must see DJANGO_DEBUG=0, a DJANGO_SECRET_KEY you did not commit, and ALLOWED_HOSTS without a star.
  3. Run heroku run python manage.py check --deploy -a invoice-web. Expect no security warnings.
  4. Open https://invoice-web.herokuapp.com/does-not-exist. Expect your 404 template, not a traceback with settings.
  5. Open the same path over http://. Expect a 301 to HTTPS.
  6. In DevTools, the session Set-Cookie must include Secure.
heroku config -a invoice-web
heroku run python manage.py check --deploy -a invoice-web
curl -sS -D - -o /dev/null http://invoice-web.herokuapp.com/ \
 | head -n 12
# Expect: HTTP/1.1 301 and a Location: https://...

If the 301 is missing, SECURE_SSL_REDIRECT is false or the proxy header is wrong. If the 404 shows a yellow page, DEBUG is still true. Fix the module. Do not add a custom error template over a live traceback and call it done.

Grep the repo for the 2021 leftovers before the next review app ships:

rg -n "DEBUG\\s*=\\s*True|django-insecure-|CORS_REPLACE_HTTPS_REFERER|SECURE_FRAME_DENY|DISABLE_COLLECTSTATIC|\\.herokuapp\\.com" \
 --glob '!static/**'

A hit is a ticket. Empty output is the receipt.

Questions we keep getting

Should I still put a new Django app on Heroku in 2026?

For a throwaway, maybe. For a product you will still own in 2028, pick a host that is still investing. The February 6 note did not publish an End of Life date. It did publish “sustaining engineering” and “no new Enterprise.” The Django settings on this page travel with you when you leave.

Is ACM enough, or do I still set SECURE_SSL_REDIRECT?

ACM puts a certificate on the hostname. It does not stop Django from answering HTTP or from sending a session cookie without Secure. Set the proxy header, the redirect, and both cookie flags. Then prove the 301.

Can ALLOWED_HOSTS be * if DEBUG is already false?

No. A star disables the Host check the setting exists for. List invoice-web.herokuapp.com and each custom name. If you think you need a wildcard, you have too many names or you are using review apps without per-app config.