What this covers
We find manage.py, install requirements.txt into a virtual environment, apply your migrations, collect your static files, and start your project with gunicorn. nginx then serves static files and uploads straight from disk rather than through Python. A failed migration stops the deploy and leaves the previous version running, so a broken release never replaces a working one.
Before you start
Your repository needs manage.py and requirements.txt in the same directory, and requirements.txt must list django — add gunicorn only if you want to pin a version, since we install it otherwise. Set STATIC_ROOT in settings.py, or neither your CSS nor the admin's own styling will be collected. Read ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS from the environment: we set both to your app's hostname, and with DEBUG off and an empty ALLOWED_HOSTS Django answers every request with 400 Bad Request. Set SECRET_KEY yourself in the app's environment variables — we deliberately do not supply one, because a key we provided would be predictable and would still sign your users' sessions.
How to do it
- Purchase an App Hosting plan or start a free trial from the Services page.
- Push your Django project to a GitHub repository. manage.py must sit at the repository root, or inside the folder you name as the Subdirectory.
- List your dependencies in requirements.txt next to manage.py: django, your database driver (psycopg[binary] for PostgreSQL), and anything else you import.
- In settings.py, read the values we provide: ALLOWED_HOSTS = [h for h in os.environ.get("ALLOWED_HOSTS", "").split(",") if h], and the same for CSRF_TRUSTED_ORIGINS. Set STATIC_ROOT = BASE_DIR / "staticfiles".
- Open Portal → Services and select your App Hosting service.
- Set Action to Deploy Python app and Runtime to python_django.
- Paste your GitHub repository URL into the Artifact Reference field, and fill in Subdirectory if your project is not at the repository root.
- Order a database: on the same service page, find the Managed PostgreSQL card in the commerce section, pick a tier and pay. It is created once the invoice is paid, and a Database tab then appears. Skip this and the starter falls back to SQLite, which is replaced on every deploy.
- Check the Database tab → Connection shows external access enabled — new databases are opened to the public endpoint automatically, so this is already done. Your app connects there on port 5432 with TLS; if it is ever switched off, the database refuses every connection even though the credentials are correct.
- Select Show connection details and copy DATABASE_URL. It already contains the host, port, user, password, database name and sslmode=require — do not rebuild it by hand.
- Add two environment variables: SECRET_KEY (generate one with python3 -c 'import secrets; print(secrets.token_urlsafe(50))') and DATABASE_URL (the string you just copied).
- Leave Build command and Start command empty. We run migrate and collectstatic ourselves, and start gunicorn on your project's own wsgi module, read from the DJANGO_SETTINGS_MODULE line in your manage.py. ⚠️ Anything you type into Start command overrides that, so only use it if you need something different — an async worker class, for example.
- Click Redeploy App and read the deploy log. It lists the migrations that applied, the static files collected, and warns you if your ALLOWED_HOSTS does not include this app's hostname.
- Visit your domain. /admin/ is the fastest check that static files are being served: if it loads but is unstyled, STATIC_ROOT is missing or collectstatic failed.
- Confirm the database is really in use: /api/info reports "vendor": "postgresql" with a server version, and the Tables tab in the portal lists django_migrations, auth_user and your own tables. ⚠️ The migration list in the deploy log looks identical on SQLite, so it does not tell you which database was used.
- Uploads survive deploys without any change on your side. Every deploy is a fresh copy of your repository, so a MEDIA_ROOT inside your project would be wiped — we move it to permanent storage the first time and say so in the log.
What our team checks
Support checks that manage.py was found in the directory you named, that migrations applied cleanly, whether collectstatic ran, that ALLOWED_HOSTS includes your hostname, and that gunicorn answered on its port before traffic was switched over.
Next step
Add STATIC_ROOT and the ALLOWED_HOSTS line to settings.py, then open the deploy form on your service page.