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:
- Add
gunicorn,whitenoiseand (for PostgreSQL)dj-database-urlandpsycopgtorequirements.txt. - Read
SECRET_KEY,DEBUG,ALLOWED_HOSTSandDATABASE_URLfrom environment variables insettings.py. - Create a web service from your repository.
- Set the Build command to
SECRET_KEY=collectstatic-only python manage.py collectstatic --noinput. - Set the Pre-deploy command to
python manage.py migrate --noinput. - Set the Start command to
gunicorn myproject.wsgi --bind 0.0.0.0:$PORT. - Add your environment variables and select Deploy.

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.pyat the root of the repository or of the folder you deploy. - A
requirements.txt(orPipfile/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:
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:
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:
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.

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:

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 installin 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 |

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:
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
- Add a managed PostgreSQL database
- Add a custom domain with automatic HTTPS, then add it to
ALLOWED_HOSTS - Enable auto-deploy on every push
- Deploy history and logs, including rollback to the previous version