Deploy a Django project

Deploy a Django project on Lucity, with migrations that run themselves and static files that do not.

Django deploys here with one command derived automatically and two things you have to decide yourself: where static files come from, and which hostnames the project trusts. Everything else is the framework working as it always does.

Start from a fresh project

python -m venv .venv && source .venv/bin/activate
pip install django gunicorn
django-admin startproject liftoff .
pip freeze > requirements.txt

requirements.txt matters more than it looks: it is how the builder recognizes the project as Python and finds gunicorn. Push the result to GitHub and connect it as described in the quickstart.

The command Lucity derives

Detection keys on manage.py plus Django in your dependencies. The start command it writes reads your WSGI_APPLICATION setting and comes out as:

python manage.py migrate && gunicorn --bind 0.0.0.0:${PORT:-8000} liftoff.wsgi:application

Read that literally, because two consequences catch people out.

Migrations run on every start. Not on every deploy: on every container start, including a restart and each replica coming up. Django's migrate is safe to repeat, so this is usually what you want. It also means a broken migration blocks the rollout instead of quietly leaving the schema behind.

No WSGI_APPLICATION, no start command. Remove or rename that setting and detection produces nothing, and the service has nowhere to start from. Either keep the setting, or set your own start command in the service settings.

If you want something else entirely, a worker pool tuned differently or collectstatic in the chain, override the start command and Lucity uses yours verbatim.

Static files are not handled for you

Nothing runs collectstatic. A project deployed as-is serves its own views correctly and 404s on /static/* in production, which is a confusing failure because it works locally with DEBUG = True.

The simplest fix is WhiteNoise, which serves static files from the app process:

pip install whitenoise
# settings.py
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    # ...
]

STATIC_ROOT = BASE_DIR / 'staticfiles'
STORAGES = {
    'staticfiles': {'BACKEND': 'whitenoise.storage.CompressedManifestStaticFilesStorage'},
}

Then put the collection step in your start command, before gunicorn:

python manage.py migrate && python manage.py collectstatic --noinput && gunicorn --bind 0.0.0.0:${PORT:-8000} liftoff.wsgi:application

For user uploads, WhiteNoise is the wrong tool. Those belong in a bucket, configured through django-storages with the S3 credentials Lucity injects. The container filesystem does not survive a rollout.

Let Django accept its own domain

Generate a domain and Django will reject requests to it until the hostname is in ALLOWED_HOSTS. Forms will also fail CSRF validation until the origin is trusted.

Read both from the environment rather than hardcoding, so the same image works in every environment:

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 to your generated hostname. Add the custom domain to the same variable when you point one at the service.

Connecting the database

Add a PostgreSQL database, then link DATABASE_URL to its fqdn-uri and let dj-database-url do the parsing:

import dj_database_url

DATABASES = {'default': dj_database_url.config(conn_max_age=600)}

psycopg[binary] belongs in your requirements alongside it. Because the value is a dynamic variable rather than a pasted string, a rotated password reaches the app without you editing anything.

Before you call it production

  • DEBUG should read from the environment and default to off. A stack trace on a public URL is a bad day.
  • SECRET_KEY belongs in a variable, not in settings.py.
  • SESSION_COOKIE_SECURE and CSRF_COOKIE_SECURE should be on. Everything Lucity serves is HTTPS, so there is no cost to it. Run python manage.py check --deploy to see the rest of the list.
  • Long migrations block the rollout. For a big one, run it once against the database and deploy after.

Next steps