Deploy a Django app

Push to GitHub, connect the repository, and TalariCloud works out how to build and start your project. This page covers the whole path — and the four things that usually break, which are the same four on every platform.

What TalariCloud works out on its own

When you connect a repository, the platform reads it before asking you anything. For a Django project it looks for manage.py, or django in your requirements, and then reads DJANGO_SETTINGS_MODULE out of manage.py to learn what your project package is called.

If gunicorn is in your requirements, that gives it a start command:

gunicorn myproject.wsgi:application --bind 0.0.0.0:$PORT

PORT is set by the platform and you cannot override it — your app must listen on whatever it is handed, which the command above does. You will see the detected runtime and start command before you create the service, and you can change either.

1. Add gunicorn

Do this first, because without it the platform falls back to python manage.py runserver and tells you so. Django's development server is single-threaded, does not serve static files safely and is explicitly not for production — Django itself says as much.

# requirements.txt
Django>=5.0
gunicorn
psycopg[binary]
whitenoise

One line, and it changes the start command from a warning into a real one.

2. Fix ALLOWED_HOSTS

This is the most common way a first Django deploy fails, and the error is unhelpfully quiet: the build succeeds, the container starts, and every request returns 400 Bad Request. Django is refusing the hostname, not crashing.

Read it from the environment rather than hard-coding it:

# settings.py
import os

ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")

CSRF_TRUSTED_ORIGINS = [
    f"https://{host}" for host in ALLOWED_HOSTS if host
]

Then set ALLOWED_HOSTS in the service's Environment tab to your TalariCloud address, plus any custom domain you have added. CSRF_TRUSTED_ORIGINS matters just as much once you have a login form — without it, signing in fails with a CSRF error on a site that otherwise looks fine.

3. Add a database

Create a managed PostgreSQL and link it to the service. The connection string arrives as DATABASE_URL, injected at deploy time — it is not something you copy into a file, and it is not stored in your repository.

# settings.py
import dj_database_url

DATABASES = {
    "default": dj_database_url.config(
        conn_max_age=600,
        ssl_require=True,
    )
}

Add dj-database-url to your requirements. A linked database always wins over a hand-typed DATABASE_URL of the same name, so a stale value left in your environment cannot quietly point production at the wrong server.

4. Run migrations as a pre-deploy step

TalariCloud does not touch your database. Nothing runs migrate for you, and that is deliberate — a platform that silently alters your schema is a platform that can silently lose your data.

Set this as the pre-deploy command in the service's settings:

python manage.py migrate --noinput

It runs after the image is built and before the new version takes traffic. If it fails, the rollout stops and the version already running keeps serving — which is the behaviour you want, because a half-migrated database serving requests is worse than a deploy that did not happen.

5. Serve static files with WhiteNoise

Django does not serve its own static files when DEBUG = False, so a deploy that otherwise works arrives with no CSS. WhiteNoise serves them from the application itself, which needs no bucket, no CDN account and no extra service.

# settings.py
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",  # directly after security
    # …the rest
]

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "staticfiles": {
        "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
    },
}

Then collect them during the build, by setting the build command:

python manage.py collectstatic --noinput

Collecting at build time rather than at start means every instance already has the files and none of them does the work twice.

6. Keep the secret key a secret

A SECRET_KEY committed to a repository is a SECRET_KEY anyone who can read that repository can use to forge sessions. Read it from the environment and set it in the Environment tab, where values are encrypted at rest and masked until you ask to see them.

# settings.py
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "") == "1"

DEBUG defaulting to off is the safer way round: a missing variable gives you a production-safe setting rather than a stack trace on a public page.

When it does not work

Every request returns 400 and the logs say nothing

ALLOWED_HOSTS, almost always — see step 2. Django rejects the hostname before your code runs, so there is nothing in your own logging to find.

The page loads but there is no styling

Static files. Either WhiteNoise is missing from MIDDLEWARE, or it is in the wrong position — it must come directly after SecurityMiddleware — or collectstatic never ran. See step 5.

The deploy fails on the pre-deploy step

That is a migration error, and the version already running is still serving. Read the deploy log, fix the migration, push again. Nothing rolled out.

"gunicorn is not in your requirements"

Exactly what it says: the platform fell back to the development server. Add gunicorn and deploy again — step 1.

The app starts and then exits

Usually a start command bound to a fixed port. It must bind 0.0.0.0 and $PORT; binding 127.0.0.1 or a hard-coded 8000 means nothing outside the container can reach it, and the health check fails.

Deploy it

Connect the repository, confirm the start command TalariCloud proposes, add the environment variables above and press Deploy. Billed in birr or USD, with a seven-day trial on eligible accounts.

Request an invitation → Open the dashboard