Deploy Django to Production: The Complete Git-Based Walkthrough
Gunicorn, migrations, collectstatic, and environment variables — all automated on every git push.
In This Guide
Deploy Django to Production: The Complete Git-Based Walkthrough (2025)
Getting Django from your local machine to a live production URL is one of those tasks that sounds simple but traditionally involves a wall of documentation: configuring Nginx, setting up Gunicorn, managing systemd services, configuring SSL manually, handling static files, setting up environment variables in shell profiles...
With git-based deployment on a managed platform, you skip all of that. Your only deployment command is git push. Everything else — Gunicorn, Nginx reverse proxy, static file collection, SSL — is handled automatically.
This guide covers the complete production deployment process for a Django app using git push deployment.
What This Covers
- Preparing Django for production (settings, security, static files)
- Setting environment variables for production (no
.envfiles on the server) - Configuring
gunicornas the production WSGI server - Handling database migrations automatically on each deploy
- Collecting static files automatically
- Setting up a custom domain with SSL
- Debugging common Django production failures
Step 1: Production-Ready Django Settings
Django ships with development settings that are unsafe in production. These changes are required before any production deployment.
1.1 Create a production settings module
The cleanest approach is to separate development and production settings:
yourproject/
settings/
__init__.py
base.py ← shared settings
development.py ← local dev overrides
production.py ← production settings
Or use a single settings.py that reads from environment variables (simpler, works well for small projects):
# settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
# Security
SECRET_KEY = os.environ['DJANGO_SECRET_KEY'] # NEVER hardcode this
DEBUG = os.environ.get('DJANGO_DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ.get('ALLOWED_HOSTS', '').split(',')
# Database — reads from DATABASE_URL environment variable
import dj_database_url
DATABASES = {
'default': dj_database_url.config(
conn_max_age=600,
conn_health_checks=True,
)
}
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
# Media files (use S3 in production)
DEFAULT_FILE_STORAGE = os.environ.get(
'DEFAULT_FILE_STORAGE',
'django.core.files.storage.FileSystemStorage'
)
# Security settings (enable in production)
if not DEBUG:
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
1.2 Add required packages
pip install gunicorn dj-database-url whitenoise psycopg2-binary
pip freeze > requirements.txt
What each package does:
- gunicorn — production WSGI server (replaces manage.py runserver)
- dj-database-url — parses DATABASE_URL environment variable into Django's DATABASES dict
- whitenoise — serves static files efficiently without a separate Nginx configuration
- psycopg2-binary — PostgreSQL adapter (if using PostgreSQL)
1.3 Configure WhiteNoise for static files
Add WhiteNoise to your middleware (before SessionMiddleware):
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Add this line
'django.contrib.sessions.middleware.SessionMiddleware',
# ... rest of middleware
]
# WhiteNoise: serve compressed, cached static files
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
WhiteNoise serves your static files directly from Python without requiring Nginx configuration for static assets. For production traffic at scale, a CDN in front of WhiteNoise is recommended, but for initial deployment it handles production load well.
1.4 Create a Procfile (for reference — configure Start Command instead)
The Procfile pattern documents your start command:
web: gunicorn yourproject.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120
On ApexWeave, configure this in Settings → Build Configuration → Start Command:
gunicorn yourproject.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120
Gunicorn worker count:
- Rule of thumb: (2 × CPU cores) + 1
- For a 1-CPU container: 3 workers
- For a 2-CPU container: 5 workers
- Start with 2–3 and increase based on response time under load
1.5 Create requirements.txt
pip freeze > requirements.txt
git add requirements.txt
git commit -m "Add production dependencies"
Step 2: Set Up .gitignore
# Python
__pycache__/
*.py[cod]
*.pyc
*.pyo
.Python
venv/
env/
.env
*.env
# Django
*.log
local_settings.py
db.sqlite3
staticfiles/
media/
# OS
.DS_Store
Thumbs.db
# IDE
.idea/
.vscode/
*.swp
Critical: Never commit .env, db.sqlite3, or staticfiles/ (generated by collectstatic).
Step 3: Configure Python Version
apexweave env:set your-app.apexweaveapp.com APEXWEAVE_STACK=python:3.12
Available: python:3.10, python:3.11, python:3.12 (default: python:3.12)
Python 3.12 is the fastest version available and supports the current Django 5.x release.
Create a .python-version file (optional, for documentation):
3.12
Step 4: Set Environment Variables
# Django core
apexweave env:set your-app.apexweaveapp.com DJANGO_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_urlsafe(50))")
apexweave env:set your-app.apexweaveapp.com DJANGO_DEBUG=False
apexweave env:set your-app.apexweaveapp.com ALLOWED_HOSTS=your-app.apexweaveapp.com,yourdomain.com
# Database
apexweave env:set your-app.apexweaveapp.com DATABASE_URL=postgres://username:password@dns.apexweaveapp.com:5432/dbname
# or MySQL:
apexweave env:set your-app.apexweaveapp.com DATABASE_URL=mysql://username:password@dns.apexweaveapp.com:3306/dbname
# Cache (if using Redis)
apexweave env:set your-app.apexweaveapp.com REDIS_URL=redis://:password@dns.apexweaveapp.com:6379/0
apexweave env:set your-app.apexweaveapp.com CACHE_BACKEND=django_redis.cache.RedisCache
# Django settings module
apexweave env:set your-app.apexweaveapp.com DJANGO_SETTINGS_MODULE=yourproject.settings
# Email
apexweave env:set your-app.apexweaveapp.com EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
apexweave env:set your-app.apexweaveapp.com EMAIL_HOST=smtp.mailgun.org
apexweave env:set your-app.apexweaveapp.com EMAIL_HOST_USER=postmaster@mg.yourdomain.com
apexweave env:set your-app.apexweaveapp.com EMAIL_HOST_PASSWORD=your-mailgun-key
# S3 storage (for media uploads)
apexweave env:set your-app.apexweaveapp.com AWS_ACCESS_KEY_ID=AKIA...
apexweave env:set your-app.apexweaveapp.com AWS_SECRET_ACCESS_KEY=xxx
apexweave env:set your-app.apexweaveapp.com AWS_STORAGE_BUCKET_NAME=your-bucket
apexweave env:set your-app.apexweaveapp.com DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage
# Verify
apexweave env:list your-app.apexweaveapp.com
Generating a secure SECRET_KEY:
python3 -c "import secrets; print(secrets.token_urlsafe(50))"
# Use the output as your DJANGO_SECRET_KEY
Step 5: Configure Build and Deploy Commands
In ApexWeave dashboard → Settings → Build Configuration:
Install Command:
pip install -r requirements.txt
Build Command:
Leave empty (Python apps don't typically have a compile step unless using frontend assets).
If you have Vite/webpack for frontend:
npm install && npm run build
Start Command:
gunicorn yourproject.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 120 --log-file -
Replace yourproject with your actual Django project name (the directory containing wsgi.py).
Post-Deployment Hook:
python manage.py migrate --no-input && python manage.py collectstatic --no-input --clear
What each command does:
- migrate --no-input — runs all pending database migrations without prompting for confirmation
- collectstatic --no-input --clear — gathers all static files into STATIC_ROOT, clears old files first
Step 6: Add Git Remote and Deploy
# Add ApexWeave remote
git remote add apexweave https://git.apexweaveapp.com/your-username/your-app.git
# Push
git push apexweave main
# Watch deployment
apexweave deploy your-app.apexweaveapp.com --follow
Expected output:
Pulling commit: c7d9f3a
Running: pip install -r requirements.txt
Collecting Django==5.0.3
...
Successfully installed Django psycopg2-binary gunicorn dj-database-url whitenoise
Running post-deployment hook:
python manage.py migrate --no-input
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, yourapp
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
...
python manage.py collectstatic --no-input --clear
131 static files copied to '/app/staticfiles'
Running: gunicorn yourproject.wsgi:application --bind 0.0.0.0:8080 --workers 2
Health check: 200 OK
Deployment complete (52 seconds)
Step 7: Verify and Debug
# Check app responds
curl https://your-app.apexweaveapp.com/
# View Django logs
apexweave logs your-app.apexweaveapp.com
# Live log stream
apexweave logs your-app.apexweaveapp.com --follow
# Interactive shell in container
apexweave bash your-app.apexweaveapp.com
# Run Django management commands
apexweave run "python manage.py shell" your-app.apexweaveapp.com
apexweave run "python manage.py createsuperuser --username admin --email admin@yourdomain.com" your-app.apexweaveapp.com
apexweave run "python manage.py check --deploy" your-app.apexweaveapp.com
python manage.py check --deploy is invaluable — it checks your settings for common production security issues and reports them clearly.
Step 8: Create Django Superuser
apexweave run "python manage.py createsuperuser" your-app.apexweaveapp.com
# You'll be prompted for username, email, and password
Common Django Production Deployment Issues
DisallowedHost error — 400 Bad Request
Cause: The Host header doesn't match ALLOWED_HOSTS.
Fix:
# Add your domain to ALLOWED_HOSTS
apexweave env:set your-app.apexweaveapp.com ALLOWED_HOSTS=your-app.apexweaveapp.com,yourdomain.com
Note: After the custom domain is set, both the ApexWeave subdomain and your custom domain should be in ALLOWED_HOSTS.
Static files returning 404
Cause: collectstatic didn't run, or WhiteNoise isn't in middleware.
Fix 1: Verify post-deployment hook includes python manage.py collectstatic --no-input.
Fix 2: Verify WhiteNoise is in MIDDLEWARE:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # Must be here
...
]
Fix 3: Verify STATIC_ROOT is set:
STATIC_ROOT = BASE_DIR / 'staticfiles'
OperationalError: no such table — database not migrated
Cause: Migrations didn't run.
Fix:
apexweave run "python manage.py migrate" your-app.apexweaveapp.com
Or ensure the post-deployment hook includes python manage.py migrate --no-input.
ImproperlyConfigured: DATABASE_URL not found
Cause: DATABASE_URL environment variable not set.
Fix:
apexweave env:list your-app.apexweaveapp.com # check it's there
apexweave env:set your-app.apexweaveapp.com DATABASE_URL=postgres://...
Gunicorn workers timing out
Cause: A view is taking too long to respond (slow query, external API call, heavy computation).
Fix:
# Increase timeout
gunicorn yourproject.wsgi:application --bind 0.0.0.0:$PORT --workers 2 --timeout 300
Or optimise the slow view (add database query optimisation, cache results, offload to a queue).
CSRF verification failed on POST requests
Cause: CSRF_COOKIE_SECURE = True is set but requests come over HTTP, or the CSRF_TRUSTED_ORIGINS doesn't include your domain.
Fix:
CSRF_TRUSTED_ORIGINS = [
'https://your-app.apexweaveapp.com',
'https://yourdomain.com',
]
Or set via environment variable:
apexweave env:set your-app.apexweaveapp.com CSRF_TRUSTED_ORIGINS=https://yourdomain.com
Django Production Security Checklist
Run this after deployment to verify security settings:
apexweave run "python manage.py check --deploy" your-app.apexweaveapp.com
Manual checklist:
- [ ] DEBUG = False
- [ ] SECRET_KEY not hardcoded (from environment)
- [ ] ALLOWED_HOSTS explicitly set (not ['*'])
- [ ] SECURE_SSL_REDIRECT = True
- [ ] SESSION_COOKIE_SECURE = True
- [ ] CSRF_COOKIE_SECURE = True
- [ ] SECURE_HSTS_SECONDS = 31536000
- [ ] Admin URL changed from default /admin/ (optional but recommended)
- [ ] Database not SQLite (use PostgreSQL or MySQL for production)
- [ ] File uploads going to S3 (not local filesystem)
- [ ] Error emails configured (ADMINS setting + email backend)
- [ ] Logging configured to persist errors
Ongoing Development Workflow
# Local development
python manage.py runserver
# Add a new feature
git add .
git commit -m "Add product search with PostgreSQL full-text search"
# Deploy to production (migrations run automatically via post-deploy hook)
git push apexweave main
apexweave deploy your-app.apexweaveapp.com --follow
# Check it worked
curl https://yourdomain.com/search/?q=test
apexweave logs your-app.apexweaveapp.com
Deploy your Django app to production at apexweave.com/git-deployment.php — Python 3.12, automatic pip installs, migration hooks, and managed SSL included.
Deploy Your App with Git Push
Automatic builds, environment variables, live logs, rollback, and custom domains. No server management required.
Deploy Free — No Card RequiredPowered by WHMCompleteSolution