Django Production Deployment: From Development to Production
Django is a batteries-included Python web framework built for rapid development, but deploying it correctly to production requires several deliberate steps that go beyond running python manage.py runserver. This guide walks through a complete, production-ready Django deployment using Docker and [PandaStack](https://pandastack.io).
Why Django Needs Special Attention in Production
Django's development server is not suitable for production — it is single-threaded, not hardened against security threats, and not designed to handle concurrent traffic. In production, Django runs behind a WSGI server like Gunicorn, which manages worker processes and handles concurrent requests efficiently.
Additionally, Django's DEBUG = True mode exposes sensitive information in error pages and should never run in production.
Project Structure
A typical production Django project should look like this:
myproject/
├── myproject/
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ └── wsgi.py
├── manage.py
├── requirements.txt
├── Dockerfile
└── pandastack.jsonSplitting settings into base and environment-specific files prevents production configuration from mixing with development defaults.
Production Settings
# myproject/settings/production.py
from .base import *
import os
DEBUG = False
ALLOWED_HOSTS = [os.environ['ALLOWED_HOST']]
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['DB_HOST'],
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
STATIC_ROOT = '/app/staticfiles'
STATICFILES_STORAGE = 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage'
SECRET_KEY = os.environ['DJANGO_SECRET_KEY']Writing a Production Dockerfile
FROM python:3.12-slim
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV DJANGO_SETTINGS_MODULE=myproject.settings.production
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN python manage.py collectstatic --noinput
EXPOSE 8000
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]Your requirements.txt should include:
django>=5.0
gunicorn>=21.0
psycopg2-binary>=2.9
whitenoise>=6.6Configuring pandastack.json
Add a pandastack.json at your project root for PandaStack to detect your health check endpoint:
{
"type": "container",
"healthCheckPath": "/health/"
}Add a corresponding health check view:
# myproject/urls.py
from django.http import JsonResponse
from django.urls import path, include
def health_check(request):
return JsonResponse({'status': 'ok'})
urlpatterns = [
path('health/', health_check),
path('admin/', admin.site.urls),
path('api/', include('api.urls')),
]Provisioning a PostgreSQL Database
PandaStack provides managed PostgreSQL instances you can provision directly from [dashboard.pandastack.io](https://dashboard.pandastack.io). Navigate to Databases → New Database → PostgreSQL. Once provisioned, copy the connection details and set them as environment variables in your project:
DB_HOST=pg.internal.pandastack.io
DB_NAME=myapp_prod
DB_USER=myapp_user
DB_PASSWORD=your-secure-password
DJANGO_SECRET_KEY=your-50-char-secret
ALLOWED_HOST=yourdomain.comRunning Migrations
Django migrations must run before or alongside your application start. The cleanest pattern is a startup script:
#!/bin/sh
# entrypoint.sh
set -e
python manage.py migrate --noinput
exec "$@"COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]Deploying via GitHub Integration
Connect your GitHub repository from the PandaStack dashboard. Every push to your production branch triggers an automatic build and deploy. Alternatively, use the CLI:
npm install -g @pandastack/cli
panda deployStatic Files with WhiteNoise
For serving static files directly from Django without a separate CDN configuration, add WhiteNoise middleware:
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
# ... rest of middleware
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'Production Checklist
DEBUG = Falsein production settingsSECRET_KEYis stored as an environment variable- Database migrations run on startup
- Static files are collected via
collectstatic - Gunicorn workers are tuned to
(2 × CPU cores) + 1 healthCheckPathreturns HTTP 200
Handling Static Files in Production
Django's development server serves static files automatically, but in production with DEBUG = False, it does not. You have two options: use WhiteNoise (simplest, no extra server needed) or serve static files from a CDN.
WhiteNoise is the recommended approach for container deployments because it requires no external configuration. Add it to your middleware and storage settings as described above, run collectstatic during the Docker build, and your app serves compressed, cache-busted static assets directly from Gunicorn workers.
Logging Configuration
Django's default logging in production should output to stdout for PandaStack to capture in the dashboard:
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'console': {
'class': 'logging.StreamHandler',
},
},
'root': {
'handlers': ['console'],
'level': 'WARNING',
},
}This ensures errors surface in your deployment logs without requiring log file access inside the container.
For full deployment documentation, visit [docs.pandastack.io](https://docs.pandastack.io).