Django Deployment Architecture
Django's built-in development server (manage.py runserver) is explicitly not designed for production use — it is single-threaded, unoptimized, and has not been audited for security. A production Django deployment requires a proper WSGI server (Gunicorn), a reverse proxy (Nginx), a production database (PostgreSQL), and careful configuration of static file serving, environment variables, and process management.
Client Request
:443 SSL + Static
WSGI Server
Application
Database
Each layer in this architecture serves a specific purpose. Nginx handles SSL termination, serves static files directly (CSS, JS, images) without touching Django, and buffers slow client connections so Gunicorn workers are freed quickly. Gunicorn manages multiple worker processes to handle concurrent requests. Django processes the business logic and database queries. PostgreSQL stores the data reliably with ACID compliance.
Server Preparation
System Updates and Python Installation
$ sudo apt install -y python3 python3-pip python3-venv python3-dev \
build-essential libpq-dev nginx
$ python3 --version
Python 3.12.3
Create a Dedicated User
Never run Django as root. Create a dedicated system user with limited privileges:
$ sudo mkdir -p /var/www/myproject
$ sudo chown django:django /var/www/myproject
PostgreSQL Database Setup
PostgreSQL is the recommended database for Django in production. It offers superior performance, reliability, and full support for Django's advanced features like JSONField, ArrayField, full-text search, and database-level constraints.
$ sudo systemctl start postgresql
$ sudo -u postgres psql
postgres=# CREATE DATABASE myproject;
postgres=# CREATE USER djangouser WITH PASSWORD 'secure-password-here';
postgres=# ALTER ROLE djangouser SET client_encoding TO 'utf8';
postgres=# ALTER ROLE djangouser SET default_transaction_isolation TO 'read committed';
postgres=# ALTER ROLE djangouser SET timezone TO 'UTC';
postgres=# GRANT ALL PRIVILEGES ON DATABASE myproject TO djangouser;
postgres=# \q
ALTER ROLE SET commands above are directly from Django's official documentation. They ensure consistent encoding (UTF-8), transaction isolation (read committed), and timezone (UTC) for all connections from this user.
Django Application Setup
Clone and Create Virtual Environment
django$ cd /var/www/myproject
django$ git clone https://github.com/youruser/myproject.git .
django$ python3 -m venv venv
django$ source venv/bin/activate
(venv) django$ pip install -r requirements.txt
Successfully installed Django-5.1 gunicorn-23.0 psycopg2-binary-2.9 ...
Configure Environment Variables
Production secrets should never be in your codebase. Use a .env file with python-dotenv or environment variables:
DEBUG=False
SECRET_KEY=your-50-character-random-secret-key-here
ALLOWED_HOSTS=example.com,www.example.com
DATABASE_URL=postgresql://djangouser:secure-password-here@localhost:5432/myproject
STATIC_ROOT=/var/www/myproject/staticfiles
MEDIA_ROOT=/var/www/myproject/media
Production Settings
Your settings.py must be configured for production security and performance:
import os
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
BASE_DIR = Path(__file__).resolve().parent.parent
DEBUG = os.getenv("DEBUG", "False") == "True"
SECRET_KEY = os.getenv("SECRET_KEY")
ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", "").split(",")
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "myproject",
"USER": "djangouser",
"PASSWORD": os.getenv("DB_PASSWORD"),
"HOST": "localhost",
"PORT": "5432",
}
}
STATIC_URL = "/static/"
STATIC_ROOT = os.getenv("STATIC_ROOT", BASE_DIR / "staticfiles")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.getenv("MEDIA_ROOT", BASE_DIR / "media")
# Security settings for production
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_SSL_REDIRECT = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
DEBUG = False is mandatory — Debug mode exposes sensitive information (settings, environment variables, full stack traces) in error pages. SECRET_KEY must be a unique, unpredictable string at least 50 characters long. Never reuse the key from development.
Run Migrations and Collect Static Files
Operations to perform:
Apply all migrations: admin, auth, contenttypes, sessions, myapp
Running migrations:
Applying contenttypes.0001_initial... OK
Applying auth.0001_initial... OK
...
(venv) django$ python manage.py collectstatic --noinput
128 static files copied to './staticfiles'.
(venv) django$ python manage.py createsuperuser
Username: admin
Email: [email protected]
Password: ********
Superuser created successfully.
Gunicorn: The WSGI Server
Gunicorn (Green Unicorn) is a Python WSGI HTTP server that manages worker processes to handle concurrent requests. It is the most popular choice for production Django deployments due to its simplicity, reliability, and performance.
Testing Gunicorn
[INFO] Starting gunicorn 23.0.0
[INFO] Listening at: http://0.0.0.0:8000
[INFO] Using worker: sync
[INFO] Booting worker with pid: 12847
Production Gunicorn Configuration
For production, create a Gunicorn configuration file that defines worker count, binding, logging, and timeouts:
# Gunicorn production configuration
import multiprocessing
bind = "unix:/var/www/myproject/gunicorn.sock"
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# Logging
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "warning"
# Process naming
proc_name = "myproject"
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
(2 x CPU_CORES) + 1. For a 4-core VPS, that means 9 workers. Each worker is an independent process that handles one request at a time (with sync workers). More workers mean more concurrent request capacity but also more memory usage — each worker typically consumes 50-150MB.
Systemd Service for Gunicorn
To ensure Gunicorn starts automatically on boot and restarts on failure, create a systemd service file:
[Unit]
Description=Gunicorn daemon for myproject
Requires=gunicorn.socket
After=network.target
[Service]
Type=notify
User=django
Group=django
RuntimeDirectory=gunicorn
WorkingDirectory=/var/www/myproject
ExecStart=/var/www/myproject/venv/bin/gunicorn \
--config gunicorn.conf.py \
myproject.wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
Restart=on-failure
RestartSec=5
KillMode=mixed
PrivateTmp=true
[Install]
WantedBy=multi-user.target
$ sudo chown django:django /var/log/gunicorn
$ sudo systemctl daemon-reload
$ sudo systemctl start gunicorn
$ sudo systemctl enable gunicorn
Created symlink ... → gunicorn.service
$ sudo systemctl status gunicorn
● gunicorn.service - Gunicorn daemon for myproject
Active: active (running)
Main PID: 15234 (gunicorn)
Tasks: 10 (limit: 4654)
Nginx Reverse Proxy Configuration
Nginx sits in front of Gunicorn, handling SSL termination, serving static and media files directly, and proxying dynamic requests to Gunicorn via the Unix socket:
upstream django_backend {
server unix:/var/www/myproject/gunicorn.sock fail_timeout=0;
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
client_max_body_size 20M;
# Static files — served directly by Nginx
location /static/ {
alias /var/www/myproject/staticfiles/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# Media files — user uploads
location /media/ {
alias /var/www/myproject/media/;
expires 7d;
}
# Everything else goes to Django via Gunicorn
location / {
proxy_pass http://django_backend;
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;
proxy_redirect off;
}
}
$ sudo nginx -t
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx
SSL with Let's Encrypt
$ sudo certbot --nginx -d example.com -d www.example.com
Successfully received certificate.
$ sudo certbot renew --dry-run
Congratulations, all simulated renewals succeeded
Deployment Automation Script
Automate your deployment process with a script that handles code updates, dependency installation, migrations, static file collection, and service restart:
#!/bin/bash
set -e
PROJECT_DIR="/var/www/myproject"
cd "$PROJECT_DIR"
echo "Pulling latest code..."
git pull origin main
echo "Activating virtual environment..."
source venv/bin/activate
echo "Installing dependencies..."
pip install -r requirements.txt --quiet
echo "Running migrations..."
python manage.py migrate --noinput
echo "Collecting static files..."
python manage.py collectstatic --noinput
echo "Restarting Gunicorn..."
sudo systemctl restart gunicorn
echo "Deployment complete!"
$ ./deploy.sh
Pulling latest code...
Already up to date.
Installing dependencies...
Running migrations...
No migrations to apply.
Collecting static files...
0 static files copied, 128 unmodified.
Restarting Gunicorn...
Deployment complete!
Troubleshooting Common Issues
502 Bad Gateway
Cause: Gunicorn is not running, or the socket file does not exist or has wrong permissions.
Solution: Check systemctl status gunicorn and verify the socket file exists at the path specified in the Nginx upstream. Ensure the Nginx user (www-data) can access the socket.
Static files return 404
Cause: collectstatic was not run, or the Nginx alias path does not match STATIC_ROOT.
Solution: Run python manage.py collectstatic and verify the path in Nginx matches exactly with the trailing slash.
DisallowedHost error
Cause: The domain is not in Django's ALLOWED_HOSTS setting.
Solution: Add the exact domain and any aliases to ALLOWED_HOSTS in settings.py. Include both example.com and www.example.com.
Gunicorn workers timing out
Cause: A view takes longer than the timeout value (default 30s).
Solution: Optimize the slow view (database queries, external API calls) or increase the timeout in gunicorn.conf.py. Consider using async workers (worker_class = "gevent") for I/O-bound workloads.
Django Deployment with Panelica
Production Checklist
DEBUG = Falsein production settings- Strong, unique
SECRET_KEYloaded from environment ALLOWED_HOSTSconfigured with exact domain names- PostgreSQL configured with Django-recommended settings
- Virtual environment with pinned dependencies (requirements.txt)
- Gunicorn with appropriate worker count and configuration file
- Systemd service for automatic start and crash recovery
- Nginx reverse proxy with static/media file serving
- SSL certificate with auto-renewal via Certbot
- Django security settings enabled (HSTS, secure cookies, CSP)
collectstaticrun after every deployment- Database migrations applied before restarting Gunicorn
- Application runs as non-root user with minimal privileges
- Log files configured with rotation