Migrating production infrastructure? Get up to ₹10,000 in migration credits. Apply now
voroaDocs
Quickstarts

Django deployment: deploy a Django app from GitHub

Deploy a Python Django app from GitHub with gunicorn, WhiteNoise and PostgreSQL. Set ALLOWED_HOSTS, run migrations safely, and go live on HTTPS in India.

To deploy a Django app on Voroa, push your project to GitHub, create a web service from the repository, set the start command to gunicorn myproject.wsgi --bind 0.0.0.0:$PORT, add your SECRET_KEY and ALLOWED_HOSTS as environment variables, and select Deploy. Voroa installs your requirements.txt, runs your build, and gives you a live HTTPS URL. Every push to your branch can then redeploy automatically.

The short version:

  1. Add gunicorn, whitenoise and (for PostgreSQL) dj-database-url and psycopg to requirements.txt.
  2. Read SECRET_KEY, DEBUG, ALLOWED_HOSTS and DATABASE_URL from environment variables in settings.py.
  3. Create a web service from your repository.
  4. Set the Build command to SECRET_KEY=collectstatic-only python manage.py collectstatic --noinput.
  5. Set the Pre-deploy command to python manage.py migrate --noinput.
  6. Set the Start command to gunicorn myproject.wsgi --bind 0.0.0.0:$PORT.
  7. Add your environment variables and select Deploy.

What runs when a Django app deploys: install requirements automatically, collectstatic as the build command, migrate as the pre-deploy command, gunicorn as the start command, then live on HTTPS

The same steps work for Flask; only the start command differs (see Deploying Flask). What every Python service includes, and what each size costs, is on Python hosting on Voroa.

What you need before you start

  • A Django project in a GitHub repository, with manage.py at the root of the repository or of the folder you deploy.
  • A requirements.txt (or Pipfile / pyproject.toml) listing every dependency.
  • A GitHub account connected to Voroa. See Connect GitHub.

You do not need a Dockerfile, a VPS or nginx. Voroa builds the image, runs gunicorn, and terminates HTTPS in front of your app.

Step 1: Make your Django project production-ready

Django’s defaults are built for your laptop. Four changes make it safe to run on the internet.

Add the production packages

Add these lines to requirements.txt:

gunicorn
whitenoise
dj-database-url
psycopg[binary]
Package Why you need it
gunicorn A production web server. Django’s runserver is for development only.
whitenoise Serves your CSS, JavaScript and images straight from your app, with no separate web server.
dj-database-url Turns the single DATABASE_URL string into Django’s DATABASES setting.
psycopg[binary] The PostgreSQL driver. Skip it if you stay on SQLite.

Read secrets and settings from the environment

Never commit SECRET_KEY or a database password. Read them from environment variables in settings.py:

python
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(",")
CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS if h]

DATABASES = {
    "default": dj_database_url.config(
        default=f"sqlite:///{BASE_DIR / 'db.sqlite3'}",
        conn_max_age=600,
    )
}

CSRF_TRUSTED_ORIGINS matters on Django 4.0 and later: without it, forms submitted over HTTPS can fail with a 403 CSRF verification failed error.

Serve static files with WhiteNoise

Add WhiteNoise directly after SecurityMiddleware, and tell Django where to collect static files:

python
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... the rest of your middleware
]

STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
    "default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
    "staticfiles": {"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"},
}

On Django versions older than 4.2, set STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" instead of STORAGES.

Trust the HTTPS proxy

Voroa terminates HTTPS in front of your app and passes the original scheme in the X-Forwarded-Proto header. Tell Django to trust it, so request.is_secure() is correct and SECURE_SSL_REDIRECT does not loop:

python
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

Commit these changes and push them to GitHub.

Step 2: Create a web service

In the Voroa dashboard, select New service, choose Web Service, and pick your repository and branch. If your Django project lives in a subfolder of the repository, set Root directory to that folder, for example /backend.

The new web service form: branch, root directory, build and start commands, pre-deploy command, health check path and instance

Step 3: Set the build, pre-deploy and start commands

Voroa detects a Python project from requirements.txt and installs your dependencies on its own. You only add the Django-specific steps:

Setting Value When it runs
Build command SECRET_KEY=collectstatic-only python manage.py collectstatic --noinput Once per deploy, while the image is built
Pre-deploy command python manage.py migrate --noinput After the build, before the new version receives traffic
Start command gunicorn myproject.wsgi --bind 0.0.0.0:$PORT Every time the service starts

You can change all three later under Settings > Build & Deploy. The pre-deploy and start commands sit together in the Deploy section:

The Deploy section of Build & Deploy settings, with the Pre-deploy command and Start command fields

Replace myproject with the folder that contains your wsgi.py.

The SECRET_KEY=collectstatic-only prefix exists because the build runs before your runtime environment variables are available, and settings.py refuses to load without a SECRET_KEY. The placeholder only lets collectstatic start; your real key is used when the app runs. Do not mark the real SECRET_KEY as a build-time variable, because that would bake it into the image.

Do not repeat pip install in the Build command. Voroa has already installed your requirements before the Build command runs. Installing them again only makes the build slower and the image larger, and the build log warns you when it happens.

Why migrations go in the pre-deploy command

Run migrate as the pre-deploy command, not in the build:

  • It runs once per deploy, with your environment variables (including DATABASE_URL) available.
  • It runs before the new version receives traffic, so new code never meets an old schema.
  • If it fails, Voroa aborts the deploy and the version already running keeps serving.
  • It runs on the same machine and private network as your service, inside the newly built version, so it reaches your database exactly the way your running service does.

Write migrations that the running version can live with for a few seconds. Add new columns first and drop old ones in a later deploy, so the old and new code both work while traffic moves across.

Bind to the port Voroa provides

Voroa sets the port in the PORT environment variable. Binding gunicorn to 0.0.0.0:$PORT is the most reliable setup: 0.0.0.0 accepts traffic from outside the container, which the loopback address 127.0.0.1 does not. See build and start commands.

Step 4: Add your environment variables

Open the service, go to Environment, and add:

Key Example value Notes
SECRET_KEY a long random string Generate one with python -c "import secrets; print(secrets.token_urlsafe(50))"
DEBUG False Never True in production; it exposes your settings in error pages
ALLOWED_HOSTS your-app.onvoroa.com Add your custom domain later, comma-separated
DATABASE_URL your PostgreSQL connection URL Only if you use a managed database

The Environment tab with write-only variables and the Add variable button

Voroa encrypts every value and never shows it again after you save it. See environment variables.

Add a PostgreSQL database

SQLite works for a first test, but its file disappears whenever a new version deploys. For real data, create a managed PostgreSQL database on Voroa and add its connection URL to your service as DATABASE_URL. Your database runs in India, next to your app.

Step 5: Deploy

Select Deploy. The build log shows each stage live: cloning your branch, installing your requirements, running collectstatic, then running migrate as the pre-deploy command. When the new version responds, Voroa switches traffic to it and your app is live at https://your-app.onvoroa.com.

Create a Django admin user

On a paid plan, connect with SSH access and run python manage.py createsuperuser.

On any plan, you can create the user from environment variables instead. Add DJANGO_SUPERUSER_USERNAME, DJANGO_SUPERUSER_EMAIL and DJANGO_SUPERUSER_PASSWORD, set the pre-deploy command for one deploy to:

bash
python manage.py migrate --noinput && python manage.py createsuperuser --noinput

Once that deploy succeeds, set the pre-deploy command back to the migrate line and delete the password variable so it is not stored longer than needed. Leaving the createsuperuser step in place makes every later deploy fail, because the user already exists.

Turn on auto-deploy and every push to your branch ships a new version.

Deploying Flask instead

Flask needs no collectstatic or migrate by default. Leave the Build command and pre-deploy command empty, and point gunicorn at your app object:

gunicorn app:app --bind 0.0.0.0:$PORT

Here app:app means “the app object in app.py”. If you use Flask-Migrate, set the pre-deploy command to flask db upgrade.

Common Django deploy errors and fixes

Error Cause Fix
DisallowedHost: Invalid HTTP_HOST header Your URL is not in ALLOWED_HOSTS Add the exact hostname to the ALLOWED_HOSTS variable, then redeploy
403 CSRF verification failed on forms CSRF_TRUSTED_ORIGINS is missing the HTTPS origin Add https://your-app.onvoroa.com (the snippet above does it for you)
Pages load without CSS Static files were not collected or not served Check the collectstatic Build command and the WhiteNoise middleware
KeyError: 'SECRET_KEY' in the build log collectstatic runs during the build, before your runtime variables are available Prefix the Build command with a placeholder: SECRET_KEY=collectstatic-only
KeyError: 'SECRET_KEY' after the deploy The variable is not set Add SECRET_KEY in Environment
relation "..." does not exist Migrations did not run Set the pre-deploy command to python manage.py migrate --noinput
ModuleNotFoundError: No module named 'psycopg' (or 'psycopg2') The PostgreSQL driver is not in requirements.txt, or the code imports psycopg2 while you installed psycopg 3 Add psycopg[binary] to requirements.txt and redeploy. Django 4.2+ works with psycopg 3; on older Django, install psycopg2-binary instead
ModuleNotFoundError: No module named 'myproject' The start command names the wrong folder Use the folder that contains wsgi.py, or fix the root directory
The build succeeds but the deploy never goes live gunicorn binds to 127.0.0.1, which only accepts traffic from inside the container Bind to 0.0.0.0:$PORT
Uploaded files vanish after a deploy The container filesystem resets on each deploy Store uploads in an external object storage service, not on the container’s filesystem

Frequently asked questions

Can I deploy Django without Docker?

Yes. Voroa detects a Python project from your requirements.txt and builds it for you. A Dockerfile is optional; use one only when you need system packages or a build Voroa does not detect. See deploy with Docker.

Should I run migrate in the build command?

No. Use the pre-deploy command. It runs once per deploy with your environment variables, before the new version receives traffic, and a failed migration stops the deploy instead of shipping broken code.

Does Voroa run collectstatic automatically?

Not on its own. Set SECRET_KEY=collectstatic-only python manage.py collectstatic --noinput as your Build command, and serve the collected files with WhiteNoise.

Can I use SQLite in production?

Only for a throwaway test. The container’s filesystem resets on every deploy, so a SQLite file is lost each time you ship. Use a managed PostgreSQL database for any data you want to keep.

Does this work for Django with Celery?

Yes. Deploy the web app as a web service, and run the Celery worker as a separate background worker from the same repository, for example with the start command celery -A myproject worker -l info. Use a managed Key-Value store as the broker.

Next steps

Last updated November 12, 2026