qeda-logo
TUTORIAL

Deploy a Django App: the Three Failures That Actually Happen.

Django builds cleanly, then dies three seconds after boot. Here are the three reasons why — and the one-line fix for each.

Amara Diallo
Amara DialloBackend Engineer · Qeda Cloud
DjangoPythonDeployment
7 min read

Django is what a lot of us learned on. It is also, unfortunately, the framework most likely to build cleanly and then die three seconds after the container starts — with an error that names a file you have never opened.

This guide deploys a Django project to Qeda from a Git repository, and spends most of its time on the three failures that actually happen, because those are what cost you the evening.

Deploying: the short version

Connect your GitHub repository from the dashboard. Qeda detects Django, installs your dependencies, collects your static files and starts the application. You get a public HTTPS URL, and the build logs stream live while it happens.

Stack detection reads your requirements.txt — you don't declare anything.

If your project is conventional — a requirements.txt, a manage.py, a WSGI entry point — that is genuinely the whole procedure. Read on anyway: the next section is the one you will come back to.

Failure 1 — your Django is older than the Python it gets

This is the most common Django deployment failure anywhere, and the error message is spectacularly unhelpful:

ModuleNotFoundError: No module named 'django.utils.six'

Nothing is wrong with your code. Django 1.x imported a compatibility library that was removed from Python years ago. The build succeeded because installing the package works fine; the crash happens at boot, when Django tries to import something that no longer exists on a modern interpreter.

The fix is to pin the interpreter your Django version was written for. Add a .python-version file at the root of your repository:

# .python-version
3.11

Which version to write depends on your Django:

  • Django 2.x → Python 3.9
  • Django 3.x → Python 3.10
  • Django 4.0 and 4.1 → Python 3.11
  • Django 4.2 → Python 3.12
  • Django 1.x → no supported Python runs it; upgrade Django instead

Qeda detects this mismatch before the build and writes the pin for you where it can, but putting the file in your repository is the honest fix — it documents the requirement for whoever clones the project next.

💡

Django 1.x needs Python 3.5 or older, which no longer receives security patches anywhere. If that is your situation, the deployment is not your real problem.

Failure 2 — SECRET_KEY is missing and nothing says so clearly

Django refuses to start without a secret key, and in production settings it usually reads one from the environment. Locally you have it in a file that .gitignore correctly excludes — so the server never sees it, and the container exits immediately.

Set it in your service's Variables tab before deploying. Qeda scans your source for the environment variables your code reads and pre-fills the ones it recognises, generating a value for the secret-looking ones so you do not have to invent one.

Variables detected from your source, with generated values for the secrets.

While you are there, set DEBUG=False and add your Qeda hostname to ALLOWED_HOSTS. A Django app with DEBUG=True in production shows a full stack trace — including settings — to anyone who triggers an error.

Failure 3 — migrations at build time

A build script that ends with python manage.py migrate cannot work, and the reason is structural rather than a bug: the build runs in an isolated sandbox with no route to your database. The database does not exist yet at that point.

Migrations belong at deploy time, when the database is reachable. Qeda detects a migration step chained into a build script, removes it from the build and runs it at deploy instead — and tells you it did so in the build log, rather than silently changing what you asked for.

• Build script runs DB migrations — moved to deploy time
  (python manage.py migrate); building with: pip install -r requirements.txt

Adding the database

From your project, add a PostgreSQL companion. Qeda provisions it and injects the connection details into your application as environment variables — DATABASE_URL, plus the individual POSTGRES_HOST, POSTGRES_USER and friends if your settings prefer them.

Read the connection from the environment, never from a hard-coded string:

import dj_database_url, os

DATABASES = {
    "default": dj_database_url.parse(os.environ["DATABASE_URL"])
}

A hard-coded host works exactly once — on your laptop.

Static files

Django does not serve its own static files in production. The usual answer is WhiteNoise, which needs no extra infrastructure: add it to your requirements, insert its middleware directly after Django's security middleware, and set STATIC_ROOT. Qeda runs collectstatic during the build when it sees the setting.

Why this matters more here

Django is heavily taught across African universities and bootcamps, which means a large share of first deployments are Django deployments — and a large share of those first deployments fail on one of the three problems above, on a platform that reports them as "build failed". People conclude they are not ready to deploy. They were ready; the error message was not.

Qeda names the cause in plain language, points at the file to change, and fixes what it can safely fix on its own. That is the whole difference between an evening lost and a link you can send someone.

Getting started

Push your project to GitHub, connect it from the dashboard, set SECRET_KEY and DEBUG=False, add a PostgreSQL companion, and deploy. If it fails, read the cause at the top of the build log — it will tell you which of these three it is.

Ready to deploy your Django app?

Connect your repository and let Qeda name the cause if anything breaks.