Django Deploy · 2025

Deploy Django to Production: The Complete Git-Based Walkthrough

Updated April 2025 · 11 min read

Gunicorn, migrations, collectstatic, and environment variables — all automated on every git push.

HomeBlog › Deploy Django to Production: The Complete Git-Based Walkthrough

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 .env files on the server)
  • Configuring gunicorn as 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 Required

Powered by WHMCompleteSolution