Docs menu
Deploying Django
Django on Primcloud: Procfile, automatic collectstatic, release-phase migrations, database URLs, and the settings tweaks Django apps need to run in a container.
On this page
Django deploys on Primcloud the same as any other Python app. Primcloud detects your repo, installs Python and dependencies, and hands the image to your Procfile. On top of that, the build gives Django one convenience: it runs collectstatic automatically when it sees manage.py and Django installed. Everything else is standard Django-in-a-container work.
Starter template#
The heroku/python-getting-started sample is a working Django app with a Procfile, a requirements.txt, and a .python-version, the fastest way to see the full flow. Fork it, import it, and you're done. See Getting started with Python for the end-to-end walkthrough.
For a more substantial starting point, heroku/python-django-sample covers Postgres, static files via WhiteNoise, and release-phase migrations, the shape most real Django apps land on.
Procfile for Django on Primcloud#
Django apps typically ship three process types:
release: python manage.py migrate --noinput
web: gunicorn myproject.wsgi --bind 0.0.0.0:$PORT
worker: celery -A myproject worker --loglevel=info
releaseruns once per deploy, after the build succeeds and before the new pods take traffic. Migrations belong here: ifmigrateexits non-zero, the release fails and Primcloud rolls back to the previous version.webruns Gunicorn against your project's WSGI app. Bind to0.0.0.0(so the port is reachable from outside the pod) and$PORT(so you bind to the port Primcloud assigned).workeris optional. Add it if you use Celery, RQ, or another async worker. Scale it independently fromwebin the dashboard.
For ASGI (Django Channels, or Django 5's async views under an ASGI server), swap Gunicorn for Uvicorn or Daphne:
web: uvicorn myproject.asgi:application --host 0.0.0.0 --port $PORT
Adjust the formation from the dashboard once your Procfile defines each process; see Pod Formations.
Env vars this framework expects#
Django reads its configuration from Python at import time. Which env vars matter depends on what your settings.py reads, but there's a common core:
| Variable | Purpose | Where it gets set |
|---|---|---|
DJANGO_SETTINGS_MODULE |
The dotted-path module Django imports for settings. Usually already set in manage.py and wsgi.py; only needed as an env var if you swap settings modules per-environment. |
Secrets panel |
SECRET_KEY |
Django's cryptographic signing key. Never commit it. Generate a fresh random string per environment. | Secrets panel |
DATABASE_URL |
Postgres/MySQL connection URL. Parsed by dj-database-url or django-environ in settings.py. |
Secrets panel |
ALLOWED_HOSTS |
Comma-separated hostnames Django will serve. Must include your Primcloud subdomain and any custom domains. | Secrets panel |
DEBUG |
False for anything production-shaped. Django refuses to run with DEBUG=True on ALLOWED_HOSTS=['*'], and DEBUG=True leaks internals in error pages. |
Secrets panel |
PORT |
The port the web process must bind to. |
Set by Primcloud; don't set it yourself. |
PYTHONUNBUFFERED |
Ensures stdout/stderr flush immediately so logs aren't lost. |
Set by Primcloud; you don't need to set it. |
A typical settings.py shape:
import os
import dj_database_url
SECRET_KEY = os.environ["SECRET_KEY"]
DEBUG = os.environ.get("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")
DATABASES = {
"default": dj_database_url.config(conn_max_age=600, ssl_require=True),
}
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
Set these in the Secrets panel in the dashboard. Every value is stored encrypted and never surfaced in build logs.
Framework gotchas on Primcloud#
Static files. collectstatic runs automatically at the end of every build, so STATIC_ROOT must be set in settings.py. A common shape is STATIC_ROOT = BASE_DIR / "staticfiles". To actually serve those files from your pods, use WhiteNoise:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
# ...
]
STORAGES = {
"staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}
WhiteNoise lets Gunicorn serve static assets directly, which is a good fit for the Primcloud model: pods don't share a filesystem with a separate static-file server.
Alternatively, ship static assets to an object store (S3, GCS, DigitalOcean Spaces) via django-storages and skip WhiteNoise entirely.
Migrations. Put python manage.py migrate --noinput in the release process. Don't run migrations from inside the web command. Every restart of every web pod would race the migration, and a failed migration wouldn't fail the deploy.
Data migrations that take a long time. Split them from the release step. Ship the schema change in one deploy (release-phase migrate), then run the data backfill separately via the Tasks feature, an idempotent management command run on-demand against your current build.
Read-only filesystem expectations. Pods have a writable filesystem that resets on restart. Anything that must persist across restarts belongs in Postgres, an object store, or an external service, not on disk. Django's default file uploads (media files) write to local disk by default; configure django-storages for anything user-uploaded.
ALLOWED_HOSTS and custom domains. When you attach a custom domain in the dashboard, add it to ALLOWED_HOSTS and redeploy. Django rejects requests with a Host header not in the list, and the failure mode is a 400 with a generic "Bad Request", which is easy to miss.
Database sslmode=require. Managed Postgres providers require TLS. dj-database-url with ssl_require=True sets ?sslmode=require on the connection string, but a hand-composed DATABASE_URL needs it added manually.
Health checks. If you configure a health check endpoint in Django, make it fast and dependency-free. A database round-trip on every check adds load and can cause flapping under load. A view that returns HttpResponse(status=200) is fine for pod-level liveness.
Troubleshooting#
collectstatic fails during build. Django imports settings before collecting, so a missing env var or an import-time error in settings.py fails the whole build. Check the traceback in the build log and set the missing variable in the Secrets panel (or add a build-time default in settings.py).
ALLOWED_HOSTS rejection after a domain change. Symptom: 400 Bad Request from Django. Add the new hostname to ALLOWED_HOSTS and redeploy.
Migrations succeed locally, fail on Primcloud. Usually a missing dependency (the version in requirements.txt differs from what's in your local venv) or a missing env var. Reproduce with the same Python version in .python-version and the exact package list in your lockfile.
"No module named 'psycopg2'" or similar C extensions. psycopg2 needs libpq-dev at install time. Either switch to psycopg2-binary (ships as a wheel, no OS deps needed) or declare libpq-dev in project.toml; see Customizing the build → Debian packages.
For anything not covered here, Troubleshooting Python has the general Python-on-Primcloud error catalog.