Deploy a Django project
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
DEBUGshould read from the environment and default to off. A stack trace on a public URL is a bad day.SECRET_KEYbelongs in a variable, not insettings.py.SESSION_COOKIE_SECUREandCSRF_COOKIE_SECUREshould be on. Everything Lucity serves is HTTPS, so there is no cost to it. Runpython manage.py check --deployto 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
- PostgreSQL for backups, the query console and connection details
- Object storage for media uploads
- Services for resources, health checks and replicas