Rohan Yeole - Homepage Rohan Yeole

Before You Deploy Your AI-Built Django App: A Production Checklist

May 8, 20261 min read

Vibe-coded Django apps work on localhost. Most are not ready for production. The gap between "it runs on my machine" and "it handles real users without losing data or leaking credentials" is where most AI-built apps fail in the first week.

This is the checklist I run through before any Django deployment — whether I built it or I'm reviewing someone else's AI-generated code.

1. DEBUG = False with a Hard Fail

# settings.py
import os

DEBUG = os.environ.get("DJANGO_DEBUG", "False") == "True"

# Hard fail if someone deploys without setting this
if not DEBUG and not os.environ.get("ALLOWED_HOSTS"):
    raise RuntimeError("ALLOWED_HOSTS must be set in production")

ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "").split(",")

DEBUG = True in production exposes stack traces (including database names and environment variables) to every visitor. Make False the hard default — the app should refuse to start in production without explicit configuration.

2. Secret Key Rotated and in the Environment

AI-generated projects almost always have SECRET_KEY hardcoded in settings.py. If that file was ever committed to git, the secret key is compromised — change it regardless.

# Generate a new one
python -c "from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())"
# settings.py
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]  # KeyError if missing — that's intentional

Store it in your server's environment, not in .env files committed to git. Check your git history: git log --all -p -- settings.py | grep SECRET_KEY.

3. PostgreSQL, Not SQLite

AI defaults to SQLite because it works with zero config. SQLite has no concurrent write support — under real traffic, you'll see database is locked errors. For anything beyond a personal side project:

# Install on Ubuntu/Debian
sudo apt install postgresql postgresql-contrib libpq-dev

# Create DB and user
sudo -u postgres psql -c "CREATE DATABASE myapp;"
sudo -u postgres psql -c "CREATE USER myapp_user WITH PASSWORD 'strongpassword';"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE myapp TO myapp_user;"
# settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
        "CONN_MAX_AGE": 60,  # persistent connections
    }
}

4. Gunicorn + Nginx, Not runserver

Django's runserver is single-threaded and not designed for production traffic. It does not handle concurrent requests, does not serve static files efficiently, and does not restart on crash.

# Install
pip install gunicorn

# Run (production)
gunicorn myapp.wsgi:application \
    --workers 3 \
    --bind 0.0.0.0:8000 \
    --timeout 120 \
    --access-logfile /var/log/gunicorn/access.log \
    --error-logfile /var/log/gunicorn/error.log
# Nginx config
server {
    listen 80;
    server_name yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name yourdomain.com;

    location /static/ {
        alias /var/www/myapp/static/;
    }

    location /media/ {
        alias /var/www/myapp/media/;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

5. Static Files Collected and Served

AI-generated apps rely on Django's dev static file server. In production with DEBUG = False, that stops working — your app loads with no CSS, no JavaScript.

# settings.py
STATIC_ROOT = BASE_DIR / "staticfiles"
STATIC_URL = "/static/"
python manage.py collectstatic --noinput

For anything beyond a small personal site, serve static files from S3 + CloudFront:

# pip install django-storages boto3
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
STATICFILES_STORAGE = "storages.backends.s3boto3.S3StaticStorage"
AWS_STORAGE_BUCKET_NAME = os.environ["AWS_S3_BUCKET"]
AWS_S3_REGION_NAME = "ap-south-1"
AWS_S3_CUSTOM_DOMAIN = os.environ.get("CLOUDFRONT_DOMAIN")

6. Media Files Not on the App Server

If users upload files (avatars, documents, images), storing them on the same server as your app means you lose them when you redeploy or the server dies. Use S3:

# All uploads go to S3 automatically with django-storages configured
DEFAULT_FILE_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
MEDIA_URL = f"https://{os.environ.get('CLOUDFRONT_DOMAIN', os.environ['AWS_S3_BUCKET'] + '.s3.amazonaws.com')}/"

For detailed S3 setup see Django + AWS S3 for Media Files.

7. Celery for Background Tasks

AI-generated code often runs slow operations synchronously in views: sending emails, generating PDFs, resizing images, making API calls. This blocks the HTTP response and times out under load.

# tasks.py
from celery import shared_task

@shared_task
def send_welcome_email(user_id):
    user = User.objects.get(id=user_id)
    # ... send email

# views.py — fire and forget
send_welcome_email.delay(user.id)
return JsonResponse({"status": "ok"})

Celery with Redis as the broker handles this correctly. See Celery + Redis setup for Django.

8. Sentry for Error Monitoring

Without error monitoring, you find out about production errors when users complain. Sentry catches every unhandled exception, groups them by type, and shows the full stack trace.

pip install sentry-sdk
# settings.py
import sentry_sdk

sentry_sdk.init(
    dsn=os.environ.get("SENTRY_DSN", ""),
    environment="production" if not DEBUG else "development",
    traces_sample_rate=0.1,
)

The free Sentry tier handles most small applications. This is the single highest-value observability tool for a small Django app.

9. Database Backups Automated

AI code doesn't include backup scripts. If your database server dies or a bad migration corrupts data, the most recent backup is all you have.

#!/bin/bash
# /etc/cron.d/db-backup — runs daily at 2am
BACKUP_DIR="/var/backups/postgres"
DB_NAME="myapp"
FILENAME="${BACKUP_DIR}/$(date +%Y%m%d_%H%M%S)_${DB_NAME}.sql.gz"

mkdir -p "$BACKUP_DIR"
pg_dump "$DB_NAME" | gzip > "$FILENAME"

# Keep 30 days
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +30 -delete

Better: use managed database services (AWS RDS, Railway, Supabase) that handle backups automatically.

10. Security Headers

Django has security middleware that adds essential HTTP headers. Enable all of it:

# settings.py — production only
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
X_FRAME_OPTIONS = "DENY"
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True

Verify with: python manage.py check --deploy — Django will report every missing security setting.

11. Rate Limiting on Auth Endpoints

AI-generated login views have no rate limiting. A bot can attempt 10,000 password guesses per minute. Use django-ratelimit:

from django_ratelimit.decorators import ratelimit

@ratelimit(key="ip", rate="5/m", method="POST", block=True)
def login_view(request):
    ...

Or handle it at the Nginx level with limit_req_zone.

12. Run python manage.py check --deploy

This is the fastest way to find missing production settings:

DJANGO_DEBUG=False python manage.py check --deploy

It will flag every security setting that's missing or misconfigured. Fix everything it reports before go-live.

The Launch Sequence

# Before deploying
python manage.py check --deploy
python manage.py test
python manage.py migrate --check  # fails if unapplied migrations exist

# Deploy
git pull
pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn
sudo systemctl restart celery

# Verify
curl -I https://yourdomain.com  # check headers
curl -s https://yourdomain.com/nonexistent/ | grep -i debug  # should be empty

If you have an AI-built Django app you want production-hardened before launch, hire me for a deployment audit and hardening. I'll go through each item on this list, fix what's wrong, and get the app running correctly on your server.

Chat with me on WhatsApp